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();① 分配对象内存 ② 调用构造方法初始化对象 ③ 把对象地址赋给 instanceif (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不变轻量级锁线程BCAS失败|↓查看对象Mark Word|┌───────┴────────┐↓ ↓锁已经释放 锁仍被A持有↓ ↓再次CAS抢锁 继续自旋↓CAS成功↓获取锁│ ├── AtomicInteger 为什么线程安全基于 CASCompare 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();线程1this old值 修改值判断old和this 不相同cas失败将this值给old下次自旋线程执行 incrementAndGet() | ↓ 读取当前值 old count | ↓ 计算 new old 1 | ↓ 执行 CAS(old,new) | | ↓ CPU原子执行 ┌─────────────────┐ │ 当前count old? │ └─────────────────┘ | ┌────┴─────┐ ↓ ↓ 是 否 ↓ ↓ 修改 CAS失败 countnew ↓ 成功 重新读取count ↓ 更新old ↓ 重新计算new ↓ 再次CAS自旋不强的情况下│ ├── LongAdder 为什么高并发性能更好AtomicLong 高并发下所有线程竞争同一个变量导致大量 CAS 失败和自旋LongAdder 采用分段思想将热点数据拆分到多个 Cell 中不同线程更新不同 Cell降低竞争最后通过 sum 汇总因此在高并发计数场景性能更高。│ └── CAS 为什么会失败│CAS执行时内存中的实际值和线程之前读取的期望值不一致说明这个值已经被其他线程修改过。CAS 的核心不是“比较操作”和“增加操作都是原子性的”而是“比较 修改交换这两个动作整体是一个不可分割的原子操作”。① 读取 oldValue② 计算 oldValue1③ CAS比较④ 修改值操作是否原子读取 count通常是原子的int读oldValue 1计算不是CAS保护的原子操作只是线程自己的计算比较当前值 oldValueCAS内部原子修改 countnewValueCAS内部原子比较 修改整体✅ CAS保证原子CAS指令比较当前值↓当前值1?↓是↓修改为2整个过程比较 修改中间不能插入其他线程。线程A读取:old1|↓计算:new2|↓CAS(1,2)-1是期望值只有一样才修改CPU原子执行┌──────────┐│比较 ││count1? │├──────────┤│修改 ││count2 │└──────────┘成功
juc线程安全场景面试题 01
发布时间:2026/7/9 16:08:51
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();① 分配对象内存 ② 调用构造方法初始化对象 ③ 把对象地址赋给 instanceif (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不变轻量级锁线程BCAS失败|↓查看对象Mark Word|┌───────┴────────┐↓ ↓锁已经释放 锁仍被A持有↓ ↓再次CAS抢锁 继续自旋↓CAS成功↓获取锁│ ├── AtomicInteger 为什么线程安全基于 CASCompare 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();线程1this old值 修改值判断old和this 不相同cas失败将this值给old下次自旋线程执行 incrementAndGet() | ↓ 读取当前值 old count | ↓ 计算 new old 1 | ↓ 执行 CAS(old,new) | | ↓ CPU原子执行 ┌─────────────────┐ │ 当前count old? │ └─────────────────┘ | ┌────┴─────┐ ↓ ↓ 是 否 ↓ ↓ 修改 CAS失败 countnew ↓ 成功 重新读取count ↓ 更新old ↓ 重新计算new ↓ 再次CAS自旋不强的情况下│ ├── LongAdder 为什么高并发性能更好AtomicLong 高并发下所有线程竞争同一个变量导致大量 CAS 失败和自旋LongAdder 采用分段思想将热点数据拆分到多个 Cell 中不同线程更新不同 Cell降低竞争最后通过 sum 汇总因此在高并发计数场景性能更高。│ └── CAS 为什么会失败│CAS执行时内存中的实际值和线程之前读取的期望值不一致说明这个值已经被其他线程修改过。CAS 的核心不是“比较操作”和“增加操作都是原子性的”而是“比较 修改交换这两个动作整体是一个不可分割的原子操作”。① 读取 oldValue② 计算 oldValue1③ CAS比较④ 修改值操作是否原子读取 count通常是原子的int读oldValue 1计算不是CAS保护的原子操作只是线程自己的计算比较当前值 oldValueCAS内部原子修改 countnewValueCAS内部原子比较 修改整体✅ CAS保证原子CAS指令比较当前值↓当前值1?↓是↓修改为2整个过程比较 修改中间不能插入其他线程。线程A读取:old1|↓计算:new2|↓CAS(1,2)-1是期望值只有一样才修改CPU原子执行┌──────────┐│比较 ││count1? │├──────────┤│修改 ││count2 │└──────────┘成功