Skip to content

保证执行安全

一、并发程序出错的根本原因

多线程操作共享变量时程序出现数据错乱、死循环、逻辑异常,根源是无法满足并发三大核心特性:原子性、内存可见性、有序性。 配套解决方案总览:

  1. 原子性:synchronized、Lock锁
  2. 内存可见性:volatile、synchronized、Lock锁
  3. 有序性: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. 锁保证原子性方案

  1. synchronized同步锁 修饰方法或代码块,将多步操作包裹为完整原子单元:
java
public synchronized void getTicket(){
    if(ticketNum <= 0){
        return ;
    }
    System.out.println(Thread.currentThread().getName()+"抢到一张票,剩余:"+ticketNum);
    ticketNum--;
}
  1. JUC Lock锁(ReentrantLock) 手动加锁、解锁,包裹所有读写共享变量的代码,保证串行执行。

三、内存可见性

1. 问题产生原理

CPU存在多级高速缓存,每个线程拥有独立本地缓存:

  1. 线程修改共享变量,数据先存放在自身本地缓存,不会立刻同步到主内存;
  2. 其他线程读取变量时,只能读取主内存旧数据,感知不到最新修改,引发死循环、数据不同步。

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同步锁(锁解决可见性)

同步锁自带内存屏障规则,天然解决可见性:

  1. 获取锁时:清空当前线程本地缓存,强制从主内存加载共享变量最新值;
  2. 释放锁时:将线程本地缓存中所有修改,全部刷新写入主内存。 只要共享变量读写操作被同一把锁包裹,修改对所有同步线程立即可见。 改造示例:
java
new Thread(()->{
    while(true){
        synchronized (VolatileDemo.class){
            if(flag){
                break;
            }
        }
    }
    System.out.println("第一个线程执行完毕...");
}).start();

(3)Lock锁(ReentrantLock,底层AQS,内存语义和synchronized一致)

  1. lock() 获取锁:清空本地缓存,强制读取主内存最新变量;
  2. 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关键字会插入内存屏障,禁止修饰变量前后指令重排,保证代码书写顺序与实际执行顺序一致;同步锁同步块内部也会限制指令重排。

五、总结

  1. 并发三大特性英文:原子性Atomic、可见性Visibility、有序性Ordering,是JUC并发安全的核心基础。
  2. 原子性:依靠synchronized、Lock锁实现,保证多步操作不可分割。
  3. 内存可见性:volatile、synchronized、Lock均可解决;锁依靠加锁读主存、解锁刷新主存实现可见性。
  4. 有序性:依靠volatile禁止指令重排。
  5. 区别:volatile只能解决可见性、有序性;synchronized和Lock可以同时满足原子性、可见性、有序性,完整保证多线程执行安全。

Powered by VitePress 1.6.4 | 持续更新中