juc线程安全场景面试题 01
2026/7/9 16:08:32 网站建设 项目流程

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保护的原子操作,只是线程自己的计算
比较当前值 == oldValueCAS内部原子
修改 count=newValueCAS内部原子
比较 + 修改整体✅ CAS保证原子

CAS指令


比较当前值

当前值==1?



修改为2

整个过程:

比较 + 修改

中间不能插入其他线程。

线程A


读取:

old=1

|

计算:

new=2

|

CAS(1,2)-1是期望值只有一样才修改


CPU原子执行

┌──────────┐
│比较 │
│count==1? │
├──────────┤
│修改 │
│count=2 │
└──────────┘


成功

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询