线程安全
基本概念
在并发编程中,原子性(Atomicity)、可见性(Visibility)和有序性(Ordering)是三个核心问题
原子性(Atomicity)
问题分析
类似数据库的事物,一批操作要么都成功,要么都失败
比如 i++
会拆成 3 个指令
- 读取主存中 i 的值到 CPU 缓存
- CPU 缓存值+1
- 将新的值写会内存
如果上述三个操作不是原子性的,多线程环境下,比如 i 初始是 1
- A、B 两个线程同时执行第一步,两个线程获取的值都是 1
- 然后个自己算各自的值,这时也都是 2
- 不管 线程A 还是 线程B 先回写已经不重要了,结果都是以最后一个线程的值为准
这时就出现问题了,两个线程执行累加(执行了两次),但结果不是 3 而是 2
解决方案
synchronized关键字:通过互斥锁保证代码块或方法的原子性
可见性(Visibility)
问题分析
线程抢夺到 CPU 后,CPU 执行线程,线程会读取主存中变量的副本保存在线程内部,线程内部具体就是 CPU 缓存
线程修改变量后再把 CPU 缓存中的刷会主存
当多线程环境下,比如线程 A、B 都有了变量 age 的副本,A 已经做完操作也刷回内存了
这时 线程B 不知道线程A已经修改了变量任使用原来的值
总结就是:一个线程对共享变量的修改,另一个线程不能立即看到
解决方案
- volatile关键字:保证变量的修改对所有线程立即可见
- synchronized:同步块不仅保证原子性,也保证可见性(释放锁前会将变量刷新到主内存)
有序性(Ordering)
问题分析
java 代码要能够运行,要先通过编译器编译成字节码,然后操作系统再把字节码翻译成可执行的指令
编译器和操作系统都会进行重排序优化(Mysql 的执行器也会把我们的sql进行优化后再执行),导致的结果就是: 执行的顺序不是我们编写的顺序
比如 双重检查锁定(DCL)问题
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 问题在这里!(这里也对应3个操作)
}
}
}
new Singleton()
的非原子操作可能被重排序为:
- 分配内存空间
- 将引用赋值给instance(此时instance≠null)
- 执行构造函数初始化
如果2和3重排序,其他线程可能拿到未初始化的对象
解决方案
-
volatile关键字:通过内存屏障禁止指令重排序
-
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");
}
}
}
});
}
}