线程安全

基本概念

在并发编程中,原子性(Atomicity)、可见性(Visibility)和有序性(Ordering)是三个核心问题

原子性(Atomicity)

问题分析

类似数据库的事物,一批操作要么都成功,要么都失败

比如 i++ 会拆成 3 个指令

  1. 读取主存中 i 的值到 CPU 缓存
  2. CPU 缓存值+1
  3. 将新的值写会内存

如果上述三个操作不是原子性的,多线程环境下,比如 i 初始是 1

  1. A、B 两个线程同时执行第一步,两个线程获取的值都是 1
  2. 然后个自己算各自的值,这时也都是 2
  3. 不管 线程A 还是 线程B 先回写已经不重要了,结果都是以最后一个线程的值为准

这时就出现问题了,两个线程执行累加(执行了两次),但结果不是 3 而是 2

解决方案

synchronized关键字:通过互斥锁保证代码块或方法的原子性

可见性(Visibility)

问题分析

线程抢夺到 CPU 后,CPU 执行线程,线程会读取主存中变量的副本保存在线程内部,线程内部具体就是 CPU 缓存

线程修改变量后再把 CPU 缓存中的刷会主存

当多线程环境下,比如线程 A、B 都有了变量 age 的副本,A 已经做完操作也刷回内存了
这时 线程B 不知道线程A已经修改了变量任使用原来的值

总结就是:一个线程对共享变量的修改,另一个线程不能立即看到

解决方案

  1. volatile关键字:保证变量的修改对所有线程立即可见
  2. synchronized:同步块不仅保证原子性,也保证可见性(释放锁前会将变量刷新到主内存)

有序性(Ordering)

问题分析

java 代码要能够运行,要先通过编译器编译成字节码,然后操作系统再把字节码翻译成可执行的指令

编译器和操作系统都会进行重排序优化(Mysql 的执行器也会把我们的sql进行优化后再执行),导致的结果就是: 执行的顺序不是我们编写的顺序

比如 双重检查锁定(DCL)问题

if (instance == null) {              // 第一次检查
    synchronized (Singleton.class) {
        if (instance == null) {      // 第二次检查
            instance = new Singleton(); // 问题在这里!(这里也对应3个操作)
        }
    }
}

new Singleton()的非原子操作可能被重排序为:

  1. 分配内存空间
  2. 将引用赋值给instance(此时instance≠null)
  3. 执行构造函数初始化

如果2和3重排序,其他线程可能拿到未初始化的对象

解决方案

  1. volatile关键字:通过内存屏障禁止指令重排序

  2. synchronized:同步块内的代码不会被重排序到同步块外

双重检查锁定正确实现

// 使用volatile关键字修饰单例实例
// 1. 保证可见性:当一个线程完成instance的初始化后,其他线程能立即看到
// 2. 禁止指令重排序:防止new Singleton()的指令被重排序导致其他线程获取到未初始化的对象
private static volatile Singleton instance;

// 私有构造函数,防止外部通过new创建实例
// 这是单例模式的关键,确保只能通过getInstance()方法获取唯一实例
private Singleton() {}

// 获取单例实例的公共静态方法
public static Singleton getInstance() {
    // 第一次检查(无锁检查)
    // 这个检查是为了提高性能:如果instance已经初始化,就直接返回,避免进入同步块
    if (instance == null) {
        // 同步代码块,锁定Singleton类对象
        // 使用类对象作为锁,确保在多线程环境下只有一个线程能进入
        synchronized (Singleton.class) {
            // 第二次检查(有锁检查)
            // 这个检查是必要的,因为可能有多个线程同时通过了第一次检查
            // 在等待锁的过程中,其他线程可能已经完成了初始化
            if (instance == null) {
                // 创建单例实例
                // 这一行代码实际上包含三个步骤:
                // 1. 分配内存空间
                // 2. 初始化对象
                // 3. 将引用指向分配的内存地址
                // 如果没有volatile,2和3可能会被重排序,导致其他线程获取到未初始化的对象
                instance = new Singleton();
            }
        }
    }
    // 返回单例实例
    return instance;
}

死锁

// 多运行几遍,还是不好复现就获取到第一个锁后 sleep
public class DeadLockDemo {  
    // 两个锁
    private static String A="A";  
    private static String B="B";  
    
    public static void main(String[] args){  
        // 线程1
        new Thread(new Runnable(){  
            @Override  
            public void run(){  
                synchronized(A){  // 拿到 A 才进入
                    synchronized(B){  // 拿到 B 才进入(这时要同时具有 A 和 B)
                        System.out.println("AB");  
                    }  
                }  
            }  
        });
        
        // 线程2
        new Thread(new Runnable(){  
            @Override  
            public void run(){  
                synchronized(B){ 
                    synchronized(A){  // 这时要同时具有 A 和 B
                        System.out.println("BA");  
                    }  
                }  
            }  
        });
    } 
}
posted @ 2024-08-26 12:28  CyrusHuang  阅读(18)  评论(0)    收藏  举报