从synchronized到CAS:Java并发必考知识点深度解析(附代码实战)

发布时间:2026/5/22 20:11:34

从synchronized到CAS:Java并发必考知识点深度解析(附代码实战) 一、synchronizedJVM内置锁的底层原理1.1 同步代码块与monitor机制synchronized可以修饰方法或代码块其底层依赖JVM的monitor监视器实现。当我们对一个代码块加锁时synchronized (lock) { // 临界区代码 }编译后会在字节码中插入monitorenter和monitorexit指令——前者在进入同步块时执行后者在正常或异常退出时执行。即使临界区抛出异常monitorexit也会被触发从而释放锁。这是一个重要的面试点抛异常不会导致锁一直占用避免死锁。1.2 锁存放在哪里——对象头Java中每个对象都有一个对象头Header其中Mark Word区域记录了锁的相关信息。对象头的大小与JVM字宽一致32位JVMMark Word占32位4字节64位JVMMark 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 什么是CASCASCompare 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(); ListThread 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的三大原子问题及解决方案1ABA问题描述线程1将值从A改为B再改回A线程2看到值还是ACAS成功但中间状态已被改变过。影响某些场景如栈顶指针、链表头可能造成错误。解决方案使用版本号。AtomicStampedReference同时保存引用和版本戳每次更新同时递增版本号。AtomicStampedReferenceString 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的面试关。加油(~^ - ^~)

相关新闻