JAVA中的多线程安全问题及解决方案
一.线程安全的定义
线程安全是指在多线程环境下,对共享资源进行并发访问时,程序能够正确地处理,不会出现数据不一致、逻辑错误等问题,确保程序的执行结果与单线程环境下的执行结果相同,或者符合预期的并发逻辑。
有些代码在多线程环境执行下会出现问题,这样的问题就称为线程不安全
二.synchronized 关键字
1.作用
核心特性
- 互斥性:同一时间只有一个线程持有锁,其他线程阻塞等待。
- 自动释放锁:退出同步块或方法时,锁自动释放。
synchronized
关键字在 Java 里用于实现同步机制,保证同一时刻只有一个线程可以访问被保护的代码块或方法。
2.用法
1.修饰方法 2.修饰代码块 (必须指定锁对象!!)
// 同步方法
public synchronized void syncMethod() {
// 操作共享资源
}
// 同步代码块
public void syncBlock() {
synchronized (lockObject) {
// 操作共享资源
}
}
三.什么是锁对象
1. synchronized
修饰静态方法
Counter
类的 increase
方法被 static synchronized
修饰,其锁对象是 Counter
类的 Class
对象,也就是 Counter.class
。示例代码如下:
1.1示例代码
class Counter {
// 将 count 定义为静态变量
private static int count = 0;
// 使用 synchronized 保证线程安全
public static synchronized void increase() {
count++;
}
public static int getCount() {
return count;
}
}
class Demo {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
Counter.increase();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
Counter.increase();
}
});
thread1.start();
thread2.start();
// 等待两个线程执行完毕
thread1.join();
thread2.join();
// 打印计数器的结果
System.out.println("最终计数结果: " + Counter.getCount());
}
}
1.2 代码结果
代码结果:(因为所持有的锁对象为同一个)
1.3 代码解释
具体到你的代码,
Counter
类的increase
方法:在多线程环境下,当一个线程进入
increase
方法时,它会尝试获取Counter.class
这个锁对象。如果该锁对象当前没有被其他线程持有,那么这个线程就可以对这个锁对象进行加锁--执行count++
操作;在这个线程执行期间,其他线程如果也想进入increase
方法,就会被阻塞,直到持有锁的线程执行完increase
方法并释放Counter.class
锁(对Counter.class这个锁对象进行解锁)。
2. synchronized
修饰实例方法
若 synchronized
修饰的是实例方法,锁对象是调用该方法的实例对象(即 this
)。示例如下:
2.1 示例代码
class Counter {
public int count = 0;
public synchronized void increase() {
count++;
}
}
class Demo14 {
private static Counter counter1 = new Counter();
private static Counter counter2 = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter1.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter2.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
// 修正输出,打印已定义的变量
System.out.println("counter1 count: " + counter1.count);
System.out.println("counter2 count: " + counter2.count);
}
}
2.2 代码结果
代码结果:(各自计数为 50000
)
2.3 代码解释
此时的两个线程持有的锁对象不是同一个。线程t1,t2持有的锁对象是counter1,counter2变量指向的不同Counter对象
3. synchronized
修饰代码块
这里的this表示谁调用了increase()方法里面,synchroized修饰的代码块就针对谁进行加锁
此时的两个线程的锁对象为同一个——>counter变量指向的Counter对象
题外话:
static 修饰引用对象变量。首先,static 修饰的变量属于类,所有实例共享。示例里的 Counter counter 被 static 修饰,是类变量,整个类共享这一个实例
class Counter {
public int count = 0;
public void increase() {
synchronized (this) {
count++;
}
}
}
class Demo14 {
private static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
// 主线程等待子线程执行完毕
t1.join();
t2.join();
System.out.println("输出结果:" + counter.count);
}
}
综上所述,锁对象的选择取决于 synchronized
的使用方式,不同的锁对象会影响同步的范围和效果。
四.原子性问题
先来看一段问题代码
1. 问题代码
class Counter {
// 将 count 定义为静态变量
private static int count = 0;
//increase定义为静态方法
public static void increase() {
count++;
}
public static int getCount() {
return count;
}
}
class Demo {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
Counter.increase();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
Counter.increase();
}
});
thread1.start();
thread2.start();
// 等待两个线程执行完毕
thread1.join();
thread2.join();
// 打印计数器的结果
System.out.println("最终计数结果: " + Counter.getCount());
}
}
1.1 运行结果
( 每次运行结果都不一样)
2.出现问题的原因
接下来说明什么是原子性
2.1 什么是原子性?
原子性指的是一个操作是不可中断的,要么全部执行,要么都不执行。在多线程环境下,如果多个线程同时修改共享变量,可能会导致数据不一致。比如 i++ 这样的操作,虽然看起来是一条语句,但实际上分为读取、增加和写入三个步骤,这就不是原子的。
原子性问题源于 CPU 指令的非原子性。例如,i++
看似是一条语句,但实际分为三步:
- 读取:从内存读取变量
i
的值。 - 增加:在 CPU 寄存*器中执行
+1
操作。 - 写入:将结果写回内存。
2.2 问题代码解释
count++
操作并非原子操作,其底层执行分为 “读取值 → 计算新值 → 写入值” 三个步骤。多线程环境下,若多个线程同时执行count++
,可能出现以下场景:
- 线程 A 读取
count
值为10
,还未执行写入;- 线程 B 也读取
count
值为10
(此时线程 A 的修改未生效);- 线程 A、B 分别计算新值为
11
并写入。最终count
只增加了1
,而非预期的增加2
,导致计数丢失。
3.解决方案
跳转到———目录1.二.三
五.内存可见性问题
- 每个线程有自己的工作内存(缓存),共享变量存储在主内存中。
- 线程操作变量时需先将变量从主内存复制到工作内存,修改后再写回主内存。
- 示例:线程 A 修改主内存中的变量
x
,若未及时写回,线程 B 的工作内存中仍保留旧值x=0
,导致可见性问题。
1. 问题代码
public class VisibilityProblem {
private static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
// 修改线程:500ms后修改flag为true
new Thread(() -> {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("修改线程:flag已设为true");
}).start();
// 读取线程:循环检查flag
new Thread(() -> {
while (!flag) {
// 空循环,等待flag变为true
}
System.out.println("读取线程:接收到flag为true");
}).start();
}
}
1.1 预期输出
修改线程:flag已设为true
读取线程:接收到flag为true
1.2 实际输出
修改线程:flag已设为true (程序不终止,读取线程无法感知flag的修改)
2. 问题原因
- 读取线程的工作内存(缓存)读取的是
flag
的旧值(false
),未从主内存更新,导致循环无法终止
3.解决办法
1.使用volatile
关键字
原理:
volatile
强制读取线程每次从主内存获取flag
的最新值,确保可见性。
修改后的代码
private static volatile boolean flag = false; // 添加volatile关键字
2.使用 synchronized
原理:
synchronized
保证同一时间只有一个线程操作flag
,且退出同步块时强制将flag
写回主内存,确保可见性。
修改后的代码
public class VisibilityProblem {
private static boolean flag = false;
// 静态锁对象(所有线程共享同一把锁)
private static final Object LOCK = new Object();
public static void main(String[] args) throws InterruptedException {
// 修改线程:500ms后修改flag为true
new Thread(() -> {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LOCK) { // 获取锁
flag = true;
System.out.println("修改线程:flag已设为true");
}
}).start();
// 读取线程:循环检查flag
new Thread(() -> {
while (true) { // 死循环+锁内检查
synchronized (LOCK) { // 获取同一把锁
if (flag) { // 锁内读取,保证可见性
System.out.println("读取线程:接收到flag为true");
break; // 退出循环
}
}
// 锁外可添加短暂休眠避免空转(非必须)
// Thread.yield();
}
}).start();
}
}
3.两者区别:
- 简单场景用
volatile
,保证可见性。但不保证原子性- 复杂场景用
synchronized
或Lock
,同时保证原子性和可见性。
六.抢占式执行问题
1.作用
wait
和notify
是Object
类的两个重要方法,用于实现线程间的通信与协作(调度线程执行顺序),它们通常和synchronized
关键字配合使用
2.wait和notify和notifyAll
1.wait()
:使当前线程进入等待状态,同时释放该线程持有的对象锁。线程会进入等待队列 ,直到其他线程调用同一对象的notify()
或notifyAll()
方法将其唤醒
2.notify()
:唤醒等待在同一对象上的一个线程。被唤醒的线程不会立即执行,而是进入阻塞队列,等待调用notify()
的线程释放锁之后,再重新竞争锁,获取到锁后才能继续执行。
3.notifyAll()
:唤醒等待在同一对象上的所有线程。这些被唤醒的线程同样会进入阻塞队列竞争锁。通常情况下,为避免某些线程长时间处于等待状态导致死锁,推荐使用notifyAll()
.使用注意事项
- 必须在同步块或同步方法中调用:
wait
和notify
方法必须在synchronized
修饰的代码块或方法中使用。因为它们依赖于对象的监视器锁(monitor),只有持有该对象锁的线程才能调用这两个方法,否则会抛出IllegalMonitorStateException
异常。 - 调用线程需持有对象锁:当线程调用
wait
方法时,它必须已经持有该对象的锁;同样,调用notify
或notifyAll
方法的线程也需要持有对象锁。
3. 极简案例:
《两个线程交替打印数字和字母(5 轮)》:
public class SimpleAlternatePrint {
private static final Object LOCK = new Object();
private static int turn = 0; // 0=数字线程,1=字母线程
public static void main(String[] args) {
// 数字线程(打印1-5)
new Thread(() -> {
for (int i = 1; i <= 5; i++) {
synchronized (LOCK) {
while (turn != 0) { // 不是自己的回合,等待
try { LOCK.wait(); } catch (InterruptedException e) { e.printStackTrace(); }
}
System.out.print(i + " "); // 打印数字
turn = 1; // 切换回合
LOCK.notify(); // 唤醒字母线程
}
}
}).start();
// 字母线程(打印A-E)
new Thread(() -> {
for (char c = 'A'; c <= 'E'; c++) {
synchronized (LOCK) {
while (turn != 1) { // 不是自己的回合,等待
try { LOCK.wait(); } catch (InterruptedException e) { e.printStackTrace(); }
}
System.out.print(c + " "); // 打印字母
turn = 0; // 切换回合
LOCK.notify(); // 唤醒数字线程
}
}
}).start();
}
}
4.复杂案例
《三个线程按顺序执行任务》:
class ThreadSequence {
private boolean isFirstDone = false;
private boolean isSecondDone = false;
// 第一个线程执行的方法
public synchronized void firstTask() throws InterruptedException {
System.out.println("First task started");
// 模拟任务执行
Thread.sleep(1000);
System.out.println("First task completed");
//把isFirstDone标志为true,第二个线程结束等待循环了,开始进入下一任务
isFirstDone = true;
notifyAll();// 1.唤醒等待在同一个对象上的所有线程 //2.第一个线程释放锁
}
// 第二个线程执行的方法
public synchronized void secondTask() throws InterruptedException {
while (!isFirstDone) {
// 等待第一个线程完成
wait();
}
System.out.println("Second task started");
// 模拟任务执行
Thread.sleep(1000);
System.out.println("Second task completed");
isSecondDone = true;
// 唤醒等待的第三个线程
notifyAll();
}
// 第三个线程执行的方法
public synchronized void thirdTask() throws InterruptedException {
while (!isSecondDone) {
// 等待第二个线程完成
wait();
}
System.out.println("Third task started");
// 模拟任务执行
Thread.sleep(1000);
System.out.println("Third task completed");
}
}
4.1 运行结果
4.2 运行顺序说明
1. First 线程执行
firstTask
持有锁 → 打印
First task started
→ 睡眠 1 秒 → 打印First task completed
→ 设置isFirstDone=true
→ 调用notifyAll()
关键:唤醒所有等待在
ThreadSequence
对象上的线程(包括 Second、Third 线程),但此时 Second/Third 尚未进入等待状态(因未获取锁)。2. Second 线程执行
secondTask
竞争锁 → 进入循环:
while (!isFirstDone)
→ 第一次检查isFirstDone=false
→ 调用wait()
→ 释放锁,进入等待队列等待:直到 First 线程调用
notifyAll()
后,Second 线程被唤醒 → 重新竞争锁 → 检查isFirstDone=true
→ 退出循环执行:打印
Second task started
→ 睡眠 1 秒 → 打印Second task completed
→ 设置isSecondDone=true
→ 调用notifyAll()
3. Third 线程执行
thirdTask
竞争锁 → 进入循环:
while (!isSecondDone)
→ 第一次检查isSecondDone=false
→ 调用wait()
→ 释放锁,进入等待队列等待:直到 Second 线程调用
notifyAll()
后,Third 线程被唤醒 → 重新竞争锁 → 检查isSecondDone=true
→ 退出循环执行:打印
Third task started
→ 睡眠 1 秒 → 打印Third task completed