[Java EE] 多线程 -- 初阶(3)
6.3 死锁
死锁的核心 : 多个线程互相持有对方所需要的锁且都不释放
① 情况一 : 一个线程获取同一把锁 , 并加锁多次
这种情况在 java 中不会出现死锁
② 情况二 : 两个线程 , 两把锁 , 每个线程获取到一把锁后 , 尝试获取另一把锁 , 会产生死锁
public class demo20 {public static void main(String[] args) throws InterruptedException {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(()->{synchronized (locker1){System.out.println("开始执行t1线程");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException();}synchronized (locker2){System.out.println("t1线程结束");}}});Thread t2 = new Thread(()->{synchronized (locker2){System.out.println("开始执行t2线程");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException();}synchronized (locker1){System.out.println("t2线程结束");}}});System.out.println("main线程开始");t1.start();t2.start();t1.join();t2.join();System.out.println("main线程结束");}
}

但为了两个线程都能获取到锁 , 必须在每个线程获取到第一个锁后加入Thread.sleep(1000) ; 两个线程相互竞争 , 互不相让 ; (家钥匙锁车里了 , 车钥匙锁家里了)
只要线程中出现交叉等待锁的情况 , 仍然可能死锁
6.4 如何解决或避免死锁
① 构成死锁的必要条件
- 锁是互斥的 , 一个线程拿到锁之后 , 另一个线程在尝试获取锁 , 必然会阻塞等待
- 锁是不可抢占的 , 线程1 拿到锁 , 线程2 也尝试获取这个锁 , 线程2 必会陷入阻塞等待
- 请求和保持 , 一个线程拿到锁1 之后 , 不释放锁1 的前提下获取锁 2 ; 也就是 使用锁的时候尽量避免嵌套
- 循环等待 , 多个线程 , 多把锁之间的等待过程构成了循环 ; A 等待 B , B 也等待 A ;
② 解决死锁
在 Java 中 synchronized 是遵守前两个条件的 ; 所以只能从后两点解决
1) 请求和保持
public class demo21 {public static void main (String[]args) throws InterruptedException {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(() -> {synchronized (locker1) {System.out.println("开始执行t1线程");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException();}}synchronized (locker2) {System.out.println("t1线程结束");}});Thread t2 = new Thread(() -> {synchronized (locker2) {System.out.println("开始执行t2线程");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException();}}synchronized (locker1) {System.out.println("t2线程结束");}});System.out.println("main线程开始");t1.start();t2.start();t1.join();t2.join();System.out.println("main线程结束");}
}

解决核心 : 避免锁嵌套
2) 循环等待
public class demo21 {public static void main (String[]args) throws InterruptedException {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(() -> {synchronized (locker1) {System.out.println("开始执行t1线程");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException();}synchronized (locker2) {System.out.println("t1线程结束");}}});Thread t2 = new Thread(() -> {synchronized (locker1) {System.out.println("开始执行t2线程");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException();}synchronized (locker2) {System.out.println("t2线程结束");}}});System.out.println("main线程开始");t1.start();t2.start();t1.join();t2.join();System.out.println("main线程结束");}
}
解决核心 : 约定每个线程加锁的顺序 , 例如 : 按序号从小到大的顺序进行加锁
7.再谈内存可见性问题
7.1 可见性
一个线程对共享变量值的修改,能够及时的被其他线程看到
7.2 内存可见性
一个线程对共享变量的修改 , 其他线程不能及时看到
这是由于 JVM 的内存模型(JMM)中 , 线程会将共享变量从主内存拷贝到自己工作内存中进行操作 , 若缺乏同步机制 , 不同线程的工作内存数据可能不一致
7.3 产生原因
① CPU 缓存机制
现代 CPU 有多级缓存 , 线程操作变量时 先再缓存中执行 , 未及时同步到主内存 , 其他线程从主内存读取的还是旧值
![]()
② 编译器优化
编译器可能对代码进行重排序或缓存变量(如将变量缓存到寄存器) , 导致变量修改对其他线程不可见
实例说明 :
import java.util.Scanner;public class demo22 {private static int flag = 0;public static void main(String[] args) {Thread t1 = new Thread(()->{while(flag == 0){}System.out.println("t1线程结束");});Thread t2 = new Thread(()->{//针对flag进行修改Scanner scanner = new Scanner(System.in);System.out.println("请输入flag的值");flag = scanner.nextInt();});t1.start();t2.start();}
}

上述代码中 , 线程2 修改 flag 后 , 线程1 可能因内存可见性问题一直卡再 while 循环中 , 无法感知 flag 的变化
解决方法
① 对 while 循环微调 , 让编译器不做优化
import java.util.Scanner;public class demo22 {private static int flag = 0;public static void main(String[] args) {Thread t1 = new Thread(()->{while(flag == 0) {try {Thread.sleep(1);}catch (InterruptedException e){throw new RuntimeException();}}System.out.println("t1线程结束");});Thread t2 = new Thread(()->{//针对flag进行修改Scanner scanner = new Scanner(System.in);System.out.println("请输入flag的值");flag = scanner.nextInt();});t1.start();t2.start();}
}


本质上 : 增加了每次 while 循环的时间 , 此时编译器就不会将 读内存操作优化为度寄存器操作了 , 因为优化的时间无足轻重
② volatile 关键字
作用 : 会禁止编译器和 CPU 的重排序优化 , 且写操作会立即同步到主内存 , 读操作会从主内存中读取 , 从而保证可见性
import java.util.Scanner;public class demo22 {private volatile static int flag = 0;public static void main(String[] args) {Thread t1 = new Thread(()->{while(flag == 0) {}System.out.println("t1线程结束");});Thread t2 = new Thread(()->{//针对flag进行修改Scanner scanner = new Scanner(System.in);System.out.println("请输入flag的值");flag = scanner.nextInt();});t1.start();t2.start();}
}
![]()
注意 : volatile 不保证原子性 , 与 sunchronized 着本质的区别
8. wait() 和 notify()
在 Java 中 wait() 和 notify()是 Object 类的方法 , 用于线程间的协作 , 通常配合 synchronized 关键字使用 , 实现线程的等待与唤醒机制
8.1 核心作用 :
wait() / wait(long timeout) :
作用 : 让当前线程释放持有的锁 , 并进入阻塞等待 , 直到其他线程调用同一对象的 notify()或 notifyAll()唤醒
wait 结束等待的条件 : ① 其他线程调用相同锁对象的 notify()方法 ; ②wait(timeout) 的等待时间超时 ; ③ 其他线程调用该线程的 interrupted 方法 , 导致 wait 抛出 InterruptedException 异常

执行结果 : 从控制台看出 , 程序执行到 wait()后进入阻塞 , 这符合 wait()方法的特性 -- 若无其他线程唤醒 , 当前线程会一直等待
notify() :
唤醒在次对象锁上等待的任意一个线程(如果是多个线程对应同一个锁对象 , 那么具体唤醒哪个是随机的) , 使其从等待状态进入就绪状态 , 重新竞争锁
使用时 务必要确保先 wait()再 notify()才会起作用 ;
notifyAll() :
唤醒在此对象锁上等待的所有线程 , 让它们重新竞争锁(但不要过于依赖)
示例 1 :
import java.util.Scanner;public class demo24 {public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(()->{try{System.out.println("wait之前");synchronized (locker1){locker1.wait();//需要借助锁对象操作}System.out.println("wait之后");}catch (InterruptedException e){throw new RuntimeException();}});Thread t2 = new Thread(()->{Scanner scanner = new Scanner(System.in);System.out.println("输入任意内容唤醒 t1");scanner.next();synchronized (locker1){locker1.notify();//同样需要锁锁对象操作}});t1.start();t2.start();}
}
使用条件 :
- 必须在 synchronized 代码块中调用
因为 wait() 和 notify() 需要操作对象的锁 , 调用前必须确保当前线程已获取该对象的锁(否则抛出异常)
- 操作的必须是同一把锁
线程 A 在锁对象lock上调用wait(),只有线程 B 在同一lock上调用notify(),才能唤醒线程 A
示例 2 :
import java.util.Scanner;public class demo25 {public static void main(String[] args) {Object locker = new Object();Thread t1 = new Thread(()->{try{System.out.println("t1 wait之前");synchronized (locker){locker.wait();}System.out.println("t1 wait之后");}catch (InterruptedException e){throw new RuntimeException();}});Thread t2 = new Thread(()->{try{System.out.println("t2 wait之前");synchronized (locker){locker.wait();}System.out.println("t2 wait之后");}catch (InterruptedException e){throw new RuntimeException();}});Thread t3 = new Thread(()->{Scanner scanner = new Scanner(System.in);System.out.println("输入任意内容唤醒所有线程");scanner.next();synchronized (locker){locker.notifyAll();}});// Thread t3 = new Thread(()->{// Scanner scanner = new Scanner(System.in);// System.out.println("输入任意内容,唤醒线程");// scanner.next();// synchronized (locker){// locker.notify();// }// System.out.println("输入任意内容,唤醒另一个线程");// scanner.next();// synchronized (locker){// locker.notify();// }//// });t1.start();t2.start();t3.start();}

8.3 与sleep()的区别
| 特性 |
|
|
| 锁释放 | 释放持有的锁 | 不释放锁 |
| 所属类 |
类 |
类 |
| 使用场景 | 线程间协作(等待 / 唤醒) | 单纯延迟执行 |
| 唤醒方式 | 需其他线程 唤醒 | 时间到后自动唤醒 |
