目录

一、上节课内容回顾

1. 核心概念

2. synchronized的使用

3. 死锁

死锁产生的条件与解决思路

二、本课重点

1. 线程安全问题分类

2. 死锁案例分析与解决

3. volatile解决内存可见性

4. wait & notify


 

一、上节课内容回顾

1. 核心概念

多线程环境下,线程安全是核心问题。导致线程不安全的原因主要有三点:

  1. 线程调度随机性

  2. 多个线程对同一个变量进行修改

  3. 修改操作不是原子的(如loadaddsave三个步骤)

针对原子性问题,解决方案是引入锁机制实现互斥/独占,通过加锁/解锁来保证操作的原子性。Java中通过synchronized关键字实现。

2. synchronized的使用

(1)修饰普通方法

this(当前对象实例)加锁。

(2)修饰static方法

给类对象加锁。

(3)可重入性

synchronized是可重入锁,同一线程可以多次获取同一把锁。

3. 死锁

死锁产生的条件与解决思路

死锁的四个必要条件(打破任意一个,即可避免死锁;通用条件,不局限于 Java)

  1. 锁是互斥的(锁的基本特点)

    对于 synchronized来说,这个特性“改不了”。
  2. 锁不可被抢占

    • 示例:A 线程获取到 locker,此时 B 线程也想获取 locker,B 把 A 的 locker抢过来 → B 持有 locker,A 线程阻塞。(这种属于可抢占)

    • 对于 synchronized来说,这个特性“改不了”。

  3. 请求和保持

    • 含义:A 线程在获取到 locker1的情况下,保持持有 locker1的状态(不释放),再尝试获取 locker2

    • 代码结构示例:

      synchronized (locker1) {
          System.out.println("t1 获取到 locker1");
          sleep(1000);
          synchronized (locker2) {
              System.out.println("t1 获取到 locker2");
          }
      }
  4. 循环等待 / 环路等待

死锁是编写多线程代码中非常典型的错误情况,主要表现为:

  1. 一个线程一把锁,连续加锁两次(重入锁只能解决这个情况)

  2. 两个线程两把锁,相互获取对方的锁

  3. M个线程N把锁(哲学家就餐问题)

二、本课重点

1. 线程安全问题分类

线程安全问题主要包括:

  • 线程调度随机

  • 多个线程修改同一个变量

  • 修改操作不是原子

  • 内存可见性

  • 指令重排序

其中,synchronized是解决线程安全问题的一种方案,但还有一种场景需要通过volatile来解决。

2. 死锁案例分析与解决

死锁场景代码示例:

public static void main(String[] args) {
    Object locker1 = new Object();
    Object locker2 = new Object();

    Thread t1 = new Thread(() -> {
        synchronized (locker1) {
            System.out.println("t1 获取到 locker1");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (locker2) {
                System.out.println("t1 获取到 locker2");
            }
        }
    });

    Thread t2 = new Thread(() -> {
        synchronized (locker2) {
            System.out.println("t2 获取到 locker2");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (locker1) {
                System.out.println("t2 获取到 locker1");
            }
        }
    });

    t1.start();
    t2.start();
}

运行结果:

Picked up JAVA_TOOL_OPTS
t1 获取到 locker1
t2 获取到 locker2
// 出现死锁,程序卡住

避免死锁的方法:打破请求和保持

避免代码中"锁的嵌套",约定好线程多把锁获取的顺序。

解决后的代码:

public static void main(String[] args) {
    Object locker1 = new Object();
    Object locker2 = new Object();

    Thread t1 = new Thread(() -> {
        synchronized (locker1) {
            System.out.println("t1 获取到 locker1");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        synchronized (locker2) {
            System.out.println("t1 获取到 locker2");
        }
    });

    Thread t2 = new Thread(() -> {
        synchronized (locker1) {  // 按照编号顺序获取锁
            System.out.println("t2 获取到 locker1");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (locker2) {
                System.out.println("t2 获取到 locker2");
            }
        }
    });

    t1.start();
    t2.start();
}

运行结果:

Picked up JAVA_TOOL_OPTS
t1 获取到 locker1
t2 获取到 locker1
t1 获取到 locker2
t2 获取到 locker2

像这样编完号之后就不会死锁。拿到锁的顺序是符合约定的顺序即可。

3. volatile解决内存可见性

synchronized是解决线程安全问题的一种方案,还有一种场景,需要通过volatile来解决——解决内存可见性引起的线程安全问题

内存可见性问题示例:

package thread;

import java.util.Scanner;

public class Demo19 {
    private static int flag = 0; // 2

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (flag == 0) {
                // 循环啥也不做,空循环
            }
            System.out.println("t1 结束");
        });

        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入 flag 的值:");
            flag = scanner.nextInt();
            System.out.println("t2 结束");
        });

        t1.start();
        t2.start();
    }
}

问题分析:

预期结果:希望程序得到什么样的结果。

实际结果:程序运行之后,真正得到的结果。

此处会出现大bug:虽然t2修改了flag,但是t1无法感知到,没有从内存读,而是从缓存/寄存器读。

使用volatile解决:

public class Demo19 {
    private static volatile int flag = 0; // 2
    // 其余代码不变...
}

什么是bug?

预期结果:希望程序得到啥样的结果。

实际结果:程序运行之后,真正得到的结果。

4. wait & notify

多线程,随机调度的。

join只能影响线程结束的顺序。

基本流程:

  • t1 和 t2,先执行某个逻辑1,t2 再去执行逻辑2

  • t1 执行到 wait()阻塞

  • t2 执行到 notify唤醒

代码实现:

while (true) {
    synchronized(locker) {
        if (条件成立) {
            执行某个任务
            break;
        } else {
            continue;
        }
    }
}

可能出现反复加锁和解锁的情况。

改进版:

while (true) {
    synchronized(locker) {
        if (条件成立) {
            执行某个任务
            break;
        } else {
            wait(); // 释放锁,进入等待状态
        }
    }
}

wait和notify是Object类的方法,并不是Thread的方法

使用任意的对象调用。

正确用法示例:

package thread;

public class Demo20 {
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();

        Thread t1 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("t1 wait 之前");
                try {
                    locker.wait(); // 必须放到synchronized内部使用
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1 wait 之后");
            }
        });

        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            synchronized (locker) {
                System.out.println("请输入任意内容,触发 notify");
                scanner.next();
                locker.notify();
                System.out.println("t2 notify 之后");
            }
        });

        t1.start();
        t2.start();
    }
}

wait做了三件事:

  1. 释放锁

  2. 等待其他线程的通知(进入阻塞状态)

  3. 通知或超时之后,从阻塞队列进入到就绪状态,并且重新获取到锁

注意:

  • wait如果不释放锁,直接抛出异常,其他线程无法进行后续操作

  • wait必须搭配锁使用,sleep不需要

  • java要求notify也得在synchronized中,操作系统的原生api则没有这个要求

notify也有一个版本,notifyAll

  • notify如果有若干个等待的线程,随机唤醒其中一个

  • notifyAll唤醒所有

多个线程等待示例:

package thread;

public class Demo23 {
    public static void main(String[] args) {
        Object locker = new Object();

        Thread t1 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("t1 wait 之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t1 wait 之后");
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("t2 wait 之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t2 wait 之后");
            }
        });

        Thread t3 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("t3 wait 之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t3 wait 之后");
            }
        });

        Thread t4 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("t4 wait 之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("t4 wait 之后");
            }
        });

        Thread t5 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("t5 notify 之前");
                Scanner scanner = new Scanner(System.in);
                System.out.println("输入任意内容进行唤醒");
                scanner.next();
                // locker.notify();
                locker.notifyAll();
                System.out.println("t5 notify 之后");
            }
        });

        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }
}

wait和sleep的区别:

  1. wait的设计是为了notify,超时时间只是"后手";sleep的设计是为了按照一定时间阻塞

  2. wait必须搭配锁使用,sleep不需要

  3. sleep进来就会先释放锁,再获取到锁,放到内核休眠,休眠时不会释放锁;wait虽然也是能够提前被interrupt唤醒,实际上更希望是通过notify唤醒(正常情况),notify唤醒之后还可以随时走,waitnotify

  4. sleepinterrupt都不是——interrupt是最可能把线程搞掉的

 

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐