9.Java并发编程避坑指南:synchronized死锁、volatile内存可见性与wait/notify正确使用
目录
一、上节课内容回顾
1. 核心概念
多线程环境下,线程安全是核心问题。导致线程不安全的原因主要有三点:
-
线程调度随机性
-
多个线程对同一个变量进行修改
-
修改操作不是原子的(如
load、add、save三个步骤)
针对原子性问题,解决方案是引入锁机制实现互斥/独占,通过加锁/解锁来保证操作的原子性。Java中通过synchronized关键字实现。
2. synchronized的使用
(1)修饰普通方法
给this(当前对象实例)加锁。
(2)修饰static方法
给类对象加锁。
(3)可重入性
synchronized是可重入锁,同一线程可以多次获取同一把锁。
3. 死锁
死锁产生的条件与解决思路
死锁的四个必要条件(打破任意一个,即可避免死锁;通用条件,不局限于 Java)
-
锁是互斥的(锁的基本特点)
对于synchronized来说,这个特性“改不了”。 -
锁不可被抢占
-
示例:A 线程获取到
locker,此时 B 线程也想获取locker,B 把 A 的locker抢过来 → B 持有locker,A 线程阻塞。(这种属于可抢占) -
对于
synchronized来说,这个特性“改不了”。
-
-
请求和保持
-
含义:A 线程在获取到
locker1的情况下,保持持有locker1的状态(不释放),再尝试获取locker2。 -
代码结构示例:
synchronized (locker1) { System.out.println("t1 获取到 locker1"); sleep(1000); synchronized (locker2) { System.out.println("t1 获取到 locker2"); } }
-
-
循环等待 / 环路等待
死锁是编写多线程代码中非常典型的错误情况,主要表现为:
-
一个线程一把锁,连续加锁两次(重入锁只能解决这个情况)
-
两个线程两把锁,相互获取对方的锁
-
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做了三件事:
-
释放锁
-
等待其他线程的通知(进入阻塞状态)
-
通知或超时之后,从阻塞队列进入到就绪状态,并且重新获取到锁
注意:
-
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的区别:
-
wait的设计是为了notify,超时时间只是"后手";sleep的设计是为了按照一定时间阻塞 -
wait必须搭配锁使用,sleep不需要 -
sleep进来就会先释放锁,再获取到锁,放到内核休眠,休眠时不会释放锁;wait虽然也是能够提前被interrupt唤醒,实际上更希望是通过notify唤醒(正常情况),notify唤醒之后还可以随时走,wait再notify -
sleep和interrupt都不是——interrupt是最可能把线程搞掉的
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)