从synchronized到CAS:Java并发必考知识点深度解析(附代码实战)
一、synchronized:JVM内置锁的底层原理
1.1 同步代码块与monitor机制
synchronized可以修饰方法或代码块,其底层依赖JVM的monitor(监视器)实现。当我们对一个代码块加锁时:
synchronized (lock) {
// 临界区代码
}
编译后会在字节码中插入`monitorenter`和`monitorexit`指令——前者在进入同步块时执行,后者在正常或异常退出时执行。即使临界区抛出异常,monitorexit也会被触发,从而释放锁。这是一个重要的面试点:抛异常不会导致锁一直占用,避免死锁。
1.2 锁存放在哪里?——对象头
Java中每个对象都有一个对象头(Header),其中Mark Word区域记录了锁的相关信息。对象头的大小与JVM字宽一致:
32位JVM:Mark Word占32位(4字节)
64位JVM:Mark Word占64位(8字节)
> 小知识:两个不同对象的hashcode有可能相同,但对象头中除了hashcode,还存储了分代年龄、锁标记等。
1.3 锁的升级(膨胀)过程 —— 必考!
synchronized在JDK 1.6之后做了重大优化,引入了锁升级机制,且升级不可逆(偏向锁→轻量级锁→重量级锁)。
| 锁状态 | 适用场景 | 原理简述 | 优缺点 |
| 偏向锁 | 只有一个线程反复获取锁 | 在Mark Word中记录线程ID,该线程再次进入时无需任何同步操作 | 优点:开销几乎为0。缺点:一旦有第二个线程竞争,立即膨胀,且需要执行偏向锁撤销(STW) |
| 轻量级锁 | 少量线程交替持有锁 | 通过CAS尝试将对象头的Mark Word复制到线程栈的Lock Record中,成功则获取锁;失败则自旋等待 | 优点:线程不用阻塞,响应快。缺点:自旋会消耗CPU,不适合大量线程竞争 |
| 重量级锁* | 多线程激烈竞争 | 竞争失败的线程进入阻塞队列,由操作系统内核完成线程调度 | 优点:不浪费CPU。缺点:线程挂起和唤醒的切换成本高(用户态↔内核态) |
为什么锁升级不可逆?因为一旦出现过竞争,JVM认为后续竞争的概率依然存在,反向降级会引入复杂度且收益很小。
1.4 锁的优点与缺点
优点:
使用简单,无需手动释放(JVM自动完成)。
JVM层面做了大量优化(锁消除、锁粗化、自适应自旋)。
与JVM调度完美整合,不会出现像ReentrantLock那样的忘记unlock。
缺点:
重量级锁下性能差于显式Lock。
锁升级过程不可控,不能像ReentrantLock那样尝试加锁(tryLock)。
偏向锁在某些场景下撤销会引发STW。
二、CAS:无锁并发的基石
2.1 什么是CAS?
CAS(Compare And Swap)是一种原子操作,需要调用操作系统内核函数才能实现真正的原子性。它包含三个参数:内存地址V,期望值A,新值B。只有当V的值等于A时,才将V更新为B,整个过程不可被中断(CPU指令级别保证)。
// 伪代码示意
boolean compareAndSwap(V, A, B) {
if (V == A) {
V = B;
return true;
}
return false;
}
2.2 代码演示:CAS实现线程安全计数器
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private AtomicInteger atomicI = new AtomicInteger(0);
private int i = 0;
public static void main(String[] args) {
final Counter cas = new Counter();
List<Thread> ts = new ArrayList<>(600);
long start = System.currentTimeMillis();
// 启动100个线程,每个对普通int和AtomicInteger各累加10000次
for (int j = 0; j < 100; j++) {
Thread t = new Thread(() -> {
for (int k = 0; k < 10000; k++) {
cas.count();
cas.safeCount();
}
});
ts.add(t);
}
for (Thread t : ts) t.start();
for (Thread t : ts) {
try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); }
}
System.out.println("非线程安全结果: " + cas.i); // 大概率小于1000000
System.out.println("CAS安全结果: " + cas.atomicI.get()); // 总是1000000
System.out.println("耗时(ms): " + (System.currentTimeMillis() - start));
}
/** 使用CAS实现线程安全计数器 */
private void safeCount() {
for (;;) {
int cur = atomicI.get();
boolean suc = atomicI.compareAndSet(cur, cur + 1);
if (suc) break;
}
}
/** 非线程安全计数器 */
private void count() { i++; }
}
讲解:普通`i++`不是原子操作(读-改-写三步),多线程下结果丢失。而CAS通过`compareAndSet`在循环中不断尝试,直到成功。这种方式避免了互斥锁的阻塞唤醒,但激烈竞争时CPU空转严重——此时反而不如重量级锁。
2.3 CAS的三大原子问题及解决方案
(1)ABA问题
描述:线程1将值从A改为B再改回A;线程2看到值还是A,CAS成功,但中间状态已被改变过。
影响:某些场景(如栈顶指针、链表头)可能造成错误。
解决方案:使用版本号。`AtomicStampedReference`同时保存引用和版本戳,每次更新同时递增版本号。
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
int[] stamp = new int[1];
String old = ref.get(stamp);
// 版本号必须匹配才能更新
ref.compareAndSet(old, "B", stamp[0], stamp[0] + 1);
(2)循环时间长开销大
自旋CAS如果长时间不成功,会大量消耗CPU。
解决:设置自旋次数上限,或退化为互斥锁(JVM自适应自旋)。
(3)只能保证一个共享变量的原子操作
解决:将多个变量封装成一个对象,使用`AtomicReference`对该对象进行CAS;或使用锁。
2.4 总线锁与缓存锁
处理器实现原子操作依赖两种锁机制:
总线锁:使用LOCK#信号,锁定整个总线,其他处理器无法访问内存。代价高。
缓存锁:锁定某个缓存行,只禁止其他处理器修改同一缓存行。性能更好。
以下两种情形不会被缓存锁定:
1. 操作的数据跨越多个缓存行(无法被单个缓存行覆盖)。
2. 处理器不支持缓存锁定(老式CPU自动降级为总线锁)。
三、内存可见性与指令重排序
3.1 为什么会看到“过期的”数据?
每个CPU核心都有自己的高速缓存。一个线程对变量的修改可能暂时停留在缓存中,没有刷新到主内存,导致另一个线程看不到修改。这就是内存可见性问题。
3.2 指令重排序
现代CPU采用流水线技术提高指令吞吐量,但会带来重排序问题。从源码到最终执行,可能经历:
源代码 → 编译器重排序 → 指令级并行重排序 → 内存系统重排序 → 最终指令序列
`final`关键字可以阻止部分重排序(构造函数返回前保证初始化完成),但范围有限。
3.3 内存屏障
JMM通过插入内存屏障(LoadLoad、StoreStore等)来禁止特定类型的重排序。`volatile`和`synchronized`底层都会插入内存屏障,从而保证可见性和有序性。
四、线程间如何通信
| 方式 | 原理 | java示例 |
| 共享内存 | 线程读写同一块内存(堆中的变量) | volatile、synchronized、CAS |
| 消息传递 | 线程间显式发送消息/事件 | wait/notify、BlockingQueue、管道流 |
> 所有实例域、静态域、数组元素都存储在堆内存中,堆是线程共享的。JMM决定一个线程对共享变量的写入何时对其它线程可见。
五、总结与面试高频追问
1、synchronized抛异常会释放锁吗?
→ 会,monitorexit指令保证释放。
2、锁升级可以降级吗?
→ 不能,不可逆。
3、CAS一定比锁快吗?
→ 不一定。低竞争时快,高竞争时自旋空转,不如重量级锁。
4、如何解决ABA?
→ AtomicStampedReference 或 AtomicMarkableReference。
5、volatile能保证原子性吗?
→ 不能,只能保证可见性和有序性。
希望这篇博客能帮你彻底拿下synchronized和CAS的面试关。加油!<(~^ - ^~)>
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)