主题切换
保证执行安全
一、并发程序出错的根本原因
多线程操作共享变量时程序出现数据错乱、死循环、逻辑异常,根源是无法满足并发三大核心特性:原子性、内存可见性、有序性。 配套解决方案总览:
- 原子性:synchronized、Lock锁
- 内存可见性:volatile、synchronized、Lock锁
- 有序性:volatile
二、原子性
1. 定义
一组操作是不可分割、不可中断的最小执行单元,CPU要么完整执行全部操作,要么完全不执行,不会执行到一半切换线程。 像ticketNum--自增操作分为读取变量、数值减一、写回内存三步,属于非原子操作,多线程并发会出现超卖、数据错乱。
2. 错误代码示例
java
int ticketNum = 10;
public void getTicket(){
if(ticketNum <= 0){
return ;
}
System.out.println(Thread.currentThread().getName()+"抢到一张票,剩余:"+ticketNum);
// 非原子操作,多线程并发会超卖
ticketNum--;
}3. 锁保证原子性方案
- synchronized同步锁 修饰方法或代码块,将多步操作包裹为完整原子单元:
java
public synchronized void getTicket(){
if(ticketNum <= 0){
return ;
}
System.out.println(Thread.currentThread().getName()+"抢到一张票,剩余:"+ticketNum);
ticketNum--;
}- JUC Lock锁(ReentrantLock) 手动加锁、解锁,包裹所有读写共享变量的代码,保证串行执行。
三、内存可见性
1. 问题产生原理
CPU存在多级高速缓存,每个线程拥有独立本地缓存:
- 线程修改共享变量,数据先存放在自身本地缓存,不会立刻同步到主内存;
- 其他线程读取变量时,只能读取主内存旧数据,感知不到最新修改,引发死循环、数据不同步。
2. 问题复现代码
java
public class VolatileDemo {
private static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while(!flag){
// 持续读取本地缓存旧值false,无限死循环
}
System.out.println("第一个线程执行完毕...");
}).start();
Thread.sleep(100);
new Thread(()->{
flag = true;
System.out.println("第二线程执行完毕...");
}).start();
}
}现象:第二个线程修改flag为true并打印结束,但第一个线程无法感知修改,持续死循环。
3. 三种解决手段,锁处理可见性核心逻辑
(1)volatile轻量关键字
修饰共享变量,写操作强制把本地缓存刷新至主内存;读操作强制从主内存读取最新数据,禁用本地缓存。仅解决可见性、有序性,无法保证原子性。
java
private static volatile boolean flag = false;(2)synchronized同步锁(锁解决可见性)
同步锁自带内存屏障规则,天然解决可见性:
- 获取锁时:清空当前线程本地缓存,强制从主内存加载共享变量最新值;
- 释放锁时:将线程本地缓存中所有修改,全部刷新写入主内存。 只要共享变量读写操作被同一把锁包裹,修改对所有同步线程立即可见。 改造示例:
java
new Thread(()->{
while(true){
synchronized (VolatileDemo.class){
if(flag){
break;
}
}
}
System.out.println("第一个线程执行完毕...");
}).start();(3)Lock锁(ReentrantLock,底层AQS,内存语义和synchronized一致)
- lock() 获取锁:清空本地缓存,强制读取主内存最新变量;
- unlock() 释放锁:本地缓存所有修改同步写入主内存; 加锁、解锁包裹共享变量读写,保证修改实时同步主存,实现内存可见性。
4. 锁实现可见性总结
synchronized、Lock依靠内存屏障,加锁清空本地缓存、解锁刷新缓存到主存,从根源解决多线程缓存不一致问题。
四、有序性
1. 指令重排定义
CPU、JIT编译器为提升执行效率,会调整代码执行顺序,单线程执行结果不变,但多线程场景会出现逻辑错乱。
java
int x;
int y;
public void actor1() {
x = 1;
y = 1;
}
public void actor2(II_Result r) {
r.r1 = y;
r.r2 = x;
}编译器可能重排先执行y=1,另一线程读取到y=1、x=0,业务逻辑出错。
2. 解决方案
volatile关键字会插入内存屏障,禁止修饰变量前后指令重排,保证代码书写顺序与实际执行顺序一致;同步锁同步块内部也会限制指令重排。
五、总结
- 并发三大特性英文:原子性Atomic、可见性Visibility、有序性Ordering,是JUC并发安全的核心基础。
- 原子性:依靠synchronized、Lock锁实现,保证多步操作不可分割。
- 内存可见性:volatile、synchronized、Lock均可解决;锁依靠加锁读主存、解锁刷新主存实现可见性。
- 有序性:依靠volatile禁止指令重排。
- 区别:volatile只能解决可见性、有序性;synchronized和Lock可以同时满足原子性、可见性、有序性,完整保证多线程执行安全。