JUC 线程安全场景面试题
│ 多个线程对共享变量的读写,因为代码是多个指令,产生了旧值读 旧值写,等语句打断产生的线程安全问题 读 → 改 → 写"
├── 一、共享变量线程安全
│ ├── i++ 为什么不是线程安全
i++不是一条 CPU 或 JVM 指令,而是由多个步骤组成,中间可能被其他线程打断。
i++线程不安全,不是因为+1有问题,而是因为它由"读 → 改 → 写"多个步骤组成,不具备原子性,多线程执行时会发生数据竞争,导致更新丢失。读 → 改 → 写
│ ├── ++i 与 i++ 区别
| 对比项 | ++i(前置自增) | i++(后置自增) |
|---|---|---|
| 执行顺序 | 先加 1,再返回值 | 先返回值,再加 1 |
| 表达式结果 | 返回新值 | 返回旧值 |
| 单独使用 | 与i++没区别 | 与++i没区别 |
| 参与表达式 | 有区别 | 有区别 |
| 线程安全 | ❌ 不安全 | ❌ 不安全 |
底层都不是一个原子操作,都包含读取 → 修改 → 写回多个步骤,线程都可能在这些步骤之间切换,因此:
++i❌ 不是线程安全i++❌ 不是线程安全
为什么i++比++i多一步?
++i不需要保存旧值
│ ├── volatile 能保证线程安全吗
volatile只能保证可见性和有序性,不能保证原子性,因此不能保证线程安全。
| 能力 | volatile | 是否解决线程安全 |
|---|---|---|
| 可见性 | ✅ 保证 | 解决部分问题 |
| 有序性(禁止指令重排序) | ✅ 保证 | 解决部分问题 |
| 原子性 | ❌ 不保证 | 无法解决 |
可见性
一个线程修改变量:其他线程能够立即看到这个修改。
代码优化,例如线程1访问共享变量1000次,就直接本地弄副本
然后共享变量改了,他也不知道,方法可以是·监听器,也可以直接访问共享变量
有序性
指令的有序
public class Singleton { private volatile static Singleton instance; public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (Singleton.class) { if (instance == null) { // 第二次检查 instance = new Singleton(); } } } return instance; } }instance = new Singleton();
① 分配对象内存 ② 调用构造方法,初始化对象 ③ 把对象地址赋给 instance
if (instance == null)
JVM 可能进行重排序
为了优化性能,可能变成:
① 分配内存
③ instance 指向内存
② 初始化对象
注意:
② 和 ③ 是可以交换的。
① 分配内存
│
▼
③ instance 指向对象
│
▼
其他线程发现 instance != null
│
▼
返回对象(危险)
│
▼
② 初始化对象(还没完成)
volatile 原理:插入内存屏障
普通操作 ↓ StoreStore屏障 ↓ volatile写 ↓ StoreLoad屏障 ↓ 后续操作volatile写之前的代码 不能跑到 volatile写之后
就可以防止其他线程拿到其他初始化完全的对象
volatile 通过缓存一致性协议保证变量修改对其他线程立即可见,通过插入内存屏障禁止 volatile 前后的指令重排序,但是它不保证复合操作的原子性,所以不能解决 i++ 这种线程安全问题。
│ ├── synchronized 如何保证线程安全
synchronized保证线程安全的核心:加锁,让同一时间只有一个线程执行临界区代码,从而保证原子性、可见性、有序性。
synchronized本质:锁的是对象的 Monitor。
所以只要是对象,都可以锁。
多个线程,对共享变量进行 读 修改 写回操作被打断产生的多个情况
多个线程变为一个线程解决问题
加锁保证原子性
同一时间只有一个线程进入,所以:
synchronized 如何保证可见性?
被锁的对象,相当于加上了volatile
有序性
JVM 不允许锁内代码随意重排序。
屏障 --还有和共享变量每次获取心值
synchronized有内存屏障。
synchronized有内存屏障。synchronized 不是让锁内代码完全禁止重排序,而是保证重排序不能破坏锁的语义。
进入锁:
monitorenter退出锁:
monitorexit它保证:
锁内操作 不会跑到锁外例如:
synchronized(lock){ a = 1; } b = 2;不能优化成:
a = 1; synchronized(lock){ } b = 2;因为这破坏锁语义。
synchronized (Singleton.class) { if (instance == null) { // 第二次检查 instance = new Singleton(); } } }出现问题
synchronized本质:基于对象监视器 Monitor 实现的互斥锁。
对象结构图片
Monitor结构
锁升级
无锁-偏向锁,线程ID不变
轻量级锁
线程B
CAS失败|
↓查看对象Mark Word
|
┌───────┴────────┐↓ ↓
锁已经释放 锁仍被A持有
↓ ↓再次CAS抢锁 继续自旋
↓CAS成功
↓
获取锁
│ ├── AtomicInteger 为什么线程安全
基于 CAS(Compare And Swap)+ volatile 实现原子操作。
CAS 是 CPU 提供的一条原子指令,比较和修改两个动作不会被线程打断。
它只是提供了对一个 int 变量进行原子操作的能力。
AtomicInteger count = new AtomicInteger(0); count.incrementAndGet();CAS(地址, 期望值, 新值) CAS(i, 0, 1) 如果当前 i == 0 那么把 i 改成 1 否则失败 整个过程是原子的: 比较 + 修改 不能被拆开。public class MyAtomicInteger { private volatile int value; public MyAtomicInteger(int value) { this.value = value; } public int incrementAndGet() { while (true) { // 1. 读取当前值 int oldValue = value; // 2. 计算新值 int newValue = oldValue + 1; // 3. CAS尝试更新 if (compareAndSet(oldValue, newValue)) { return newValue; } // 4. 失败,重新循环 } } public boolean compareAndSet(int expect, int update) { if (value == expect) { value = update; return true; } return false; } }if(value == expect){ value = update; }不是真正 CAS。
因为:
比较 和 赋值 不是原子的JDK AtomicInteger 真正实现
public class AtomicInteger extends Number {
private volatile int value;
public final int incrementAndGet() {return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
}
AtomicInteger count = new AtomicInteger(1); count.incrementAndGet();count.incrementAndGet();
线程1
this old值 修改值
判断old和this 不相同cas失败,将this值给old
下次自旋
线程执行 incrementAndGet() | ↓ 读取当前值 old = count | ↓ 计算 new = old + 1 | ↓ 执行 CAS(old,new) | | ↓ CPU原子执行: ┌─────────────────┐ │ 当前count == old? │ └─────────────────┘ | ┌────┴─────┐ ↓ ↓ 是 否 ↓ ↓ 修改 CAS失败 count=new ↓ 成功 重新读取count ↓ 更新old ↓ 重新计算new ↓ 再次CAS自旋不强的情况下
│ ├── LongAdder 为什么高并发性能更好
AtomicLong 高并发下所有线程竞争同一个变量,导致大量 CAS 失败和自旋;LongAdder 采用分段思想,将热点数据拆分到多个 Cell 中,不同线程更新不同 Cell,降低竞争,最后通过 sum 汇总,因此在高并发计数场景性能更高。
│ └── CAS 为什么会失败
│
CAS执行时,内存中的实际值和线程之前读取的期望值不一致,说明这个值已经被其他线程修改过。
CAS 的核心不是“比较操作”和“增加操作都是原子性的”,而是“比较 + 修改(交换)这两个动作整体是一个不可分割的原子操作”。
① 读取 oldValue
② 计算 oldValue+1
③ CAS比较
④ 修改值
操作 是否原子 读取 count 通常是原子的(int读) oldValue + 1计算 不是CAS保护的原子操作,只是线程自己的计算 比较当前值 == oldValue CAS内部原子 修改 count=newValue CAS内部原子 比较 + 修改整体 ✅ CAS保证原子
CAS指令
比较当前值↓
当前值==1?
↓是
↓修改为2
整个过程:
比较 + 修改中间不能插入其他线程。
线程A
读取:old=1
|
↓计算:
new=2
|
↓CAS(1,2)-1是期望值只有一样才修改
CPU原子执行┌──────────┐
│比较 │
│count==1? │
├──────────┤
│修改 │
│count=2 │
└──────────┘
成功