深入理解 synchronized

发布时间:2026/11/2 15:24:12
深入理解 synchronized 一、前置通用锁策略基础锁策略不是 Java 独有是所有并发编程的通用概念。synchronized内部的锁升级本质就是在不同场景下自动切换这些策略。1.1 乐观锁 vs 悲观锁悲观锁总是假设最坏的情况每次去拿数据的时候都认为别人会修改所以每次在拿数据的时候都会上锁这样别人想拿这个数据就会阻塞直到它拿到锁。乐观锁假设数据一般情况下不会产生并发冲突所以在数据进行提交更新的时候才会正式对数据是否产生并发冲突进行检测如果发现并发冲突了则让返回用户错误的信息让用户决定如何去做。Synchronized 初始使用乐观锁策略. 当发现锁竞争比较频繁的时候, 就会自动切换成悲观锁策略.1.2 重量级锁 vs 轻量级锁锁的核心特性 原子性, 这样的机制追根溯源是 CPU 这样的硬件设备提供的.CPU 提供了 原子操作指令.操作系统基于 CPU 的原子指令, 实现了 mutex 互斥锁.JVM 基于操作系统提供的互斥锁, 实现了 synchronized 和 ReentrantLock 等关键字和类注意, synchronized 并不仅仅是对 mutex 进行封装, 在 synchronized 内部还做了很多其他的工作重量级锁加锁机制重度依赖了 OS 提供了 mutex。轻量级锁 加锁机制尽可能不使用 mutex, 而是尽量在用户态代码完成. 实在搞不定了, 再使用 mutex。理解用户态 vs 内核态想象去银行办业务。在窗口外, 自己做, 这是用户态。用户态的时间成本是比较可控的。在窗口内, 工作人员做, 这是内核态。内核态的时间成本是不太可控的。如果办业务的时候反复和工作人员沟通, 还需要重新排队, 这时效率是很低的。1.3 自旋锁 vs 挂起等待锁自旋锁抢锁失败不挂起循环不断重试抢锁锁释放后第一时间就能拿到。 优点不放弃 CPU避免线程调度开销缺点锁持有时间长时空转浪费 CPU。挂起等待锁抢锁失败直接放弃 CPU 被挂起等锁释放后再被唤醒。 优点不消耗 CPU缺点线程唤醒、调度开销大。1.4 公平锁 vs 非公平锁公平锁严格先来后到先排队的线程先拿锁不会出现线程饥饿。非公平锁锁释放后新线程和排队线程一起抢不保证顺序。 优点吞吐量更高缺点极端场景下部分线程长期抢不到锁。synchronized是非公平锁ReentrantLock默认非公平可手动开启公平模式。1.5 可重入锁 vs 不可重入锁可重入锁同一个线程可以多次获取同一把锁不会自己把自己锁死。 内部维护计数器加锁 1解锁 - 1归零才真正释放锁。不可重入锁同一个线程第二次获取同一把锁会直接阻塞造成死锁。synchronized、ReentrantLock都是可重入锁。二、CAS乐观锁的核心实现偏向锁、轻量级锁的底层都依赖 CAS 硬件指令。2.1 CAS 是什么CAS 全称Compare And Swap比较并交换是一条 CPU 硬件原子指令全程原子执行不需要操作系统加锁。我们假设内存中的原数据V旧的预期值A需要修改的新值B。1.比较A与V是否相等。比较2. 如果比较相等将B写入V。交换3. 返回操作是否成功。CAS 伪代码假代码不能编译运行表示逻辑下面写的代码不是原子的, 真实的 CAS 是一个原子的硬件指令完成的. 这个伪代码只是辅助理解 CAS的工作流程.boolean CAS(address, expectValue, swapValue) { if (address expectedValue) { address swapValue; return true; } return false; }举例比较读取内存中count的值当前是 0和预期旧值 0 相等说明期间没有被其他线程修改。交换把内存中的值更新为 1。返回操作成功返回true。2.2 CAS 是怎么实现的针对不同的操作系统JVM 用到了不同的 CAS 实现原理简单来讲• java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作• unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg• Atomic::cmpxchg 的实现使用了汇编的 CAS 操作并使用 cpu 硬件提供的 lock 机制保证其原子性。简而言之是因为硬件予以了支持软件层面才能做到。java.util.concurrent.atomic包下的原子类全部基于 CAS 实现最常用的是AtomicInteger。2.3 典型应用01 原子类package thread; import java.util.concurrent.atomic.AtomicInteger; public class AtomicDemo { private static AtomicInteger count new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { Thread t1 new Thread(() - { for (int i 0; i 50000; i) { // 相当于 i底层 CAS 自旋重试 count.getAndIncrement(); } }); Thread t2 new Thread(() - { for (int i 0; i 50000; i) { count.getAndIncrement(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count count.get()); // 结果恒为 100000 } }02 实现自旋锁基于 CAS 实现更灵活的锁, 获取到更多的控制权伪代码public class SpinLock { private Thread owner null; public void lock(){ // 通过 CAS 看当前锁是否被某个线程持有. // 如果这个锁已经被别的线程持有, 那么就自旋等待. // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. while(!CAS(this.owner, null, Thread.currentThread())){ } } public void unlock (){ this.owner null; } }2.4 CAS 的 ABA 问题CAS 的核心逻辑是比较「内存当前值」和「我预期的旧值」是否相等相等就认为没被改过执行更新。 ABA 问题就出在这里值看起来没变但中间其实被修改过最后又改回了原值CAS 无法感知这个变化过程。完整流程拆解线程 T1 读取共享变量得到当前值 A记录为自己的预期旧值T1 执行业务逻辑准备通过 CAS 将 A 更新为目标新值在此期间线程 T2 介入先将变量从 A 修改为 B经过一系列操作后又把变量改回了 AT1 最终执行 CAS 时发现内存当前值仍然是 A误以为数据从未被改动更新成功。从 CAS 指令的视角看操作完全合法且成功但从业务视角看数据已经经历了一轮「改出又改回」的过程中间的所有变化都被彻底掩盖了。 通俗类比翻新手机 你收到一台手机外观、系统显示都是全新状态 A。你以为它是从未拆封的新机但其实它被人使用过一段时间状态 B又被商家翻新回了全新外观状态 A。只看表面值你完全分辨不出来但它中间已经被改动过。2.4.2 ABA 一定会引发故障吗并不是所有场景下 ABA 都会导致业务错误需要区分场景对待无影响场景如果业务只关心「最终值」不关心数据的变化过程比如简单的计数器自增ABA 问题不会对结果造成任何影响无需额外处理。高风险场景如果业务依赖数据的「状态流转过程」比如金额变动、订单状态切换、无锁链表 / 节点操作ABA 可能会引发严重的逻辑 BUG。2.4.3 经典业务案例ATM 重复扣款我们以最经典的 ATM 取款场景为例直观感受 ABA 问题带来的实际业务损失初始条件用户账户余额 100 元取款机出现并发异常同时发起两个扣款 50 元的线程预期结果一个线程扣款成功另一个线程扣款失败最终余额 50 元。正常流程无 ABA 干扰线程 1、线程 2 都读取到余额 100 元作为各自的预期旧值线程 1 先执行 CAS 扣款成功余额更新为 50 元线程 2 再执行 CAS发现当前余额 50 ≠ 预期 100扣款失败。结果正确最终余额 50 元。异常流程ABA 导致重复扣款线程 1、线程 2 都读取到余额 100 元线程 1 先执行 CAS 扣款成功余额变为 50 元此时线程 2 因 CPU 调度延迟还未执行 CAS中途有第三方向账户转账 50 元余额从 50 元又变回了 100 元完成了「100 → 50 → 100」的 ABA 循环线程 2 终于执行 CAS发现当前余额 100 与自己的预期旧值一致扣款成功余额再次变为 50 元。最终结果用户原本只有 100 元经历一次取款、一次等额转账后余额只剩下 50 元平白损失了 50 元。问题的根源就在于CAS 只校验了金额数值没有校验「金额是否被修改过」。2.4.4 标准解决方案版本号机制解决 ABA 问题的核心思路非常朴素不只比较值还要比较数据的版本号。具体规则给每个共享数据搭配一个版本号每次修改数据时版本号自动 1执行 CAS 时必须同时满足「值相等 版本号相等」两个条件才允许更新哪怕值完全相同只要版本号变大了就说明数据被修改过操作直接失败。对应到上面的取款场景初始余额 100版本号 1线程 1 扣款成功余额 50版本号 2转账入账余额 100版本号 3线程 2 用旧版本号 1 执行 CAS版本不匹配扣款失败三、synchronized基本特点synchronized是Java 内置的互斥同步关键字属于 JVM 层面实现的锁用来解决多线程并发访问共享资源的线程安全问题它可以保证同一时刻只有一个线程执行被锁住的代码保证共享变量操作的原子性、可见性、有序性。锁的是对象不是代码自动加锁、自动释放锁3.1 原子性被 synchronized 包裹的代码是一个不可分割的整体要么全部执行完要么不执行同一时刻只有一个线程能进入同步块保证共享变量操作不可分割。例i本身不是原子操作加上 synchronized 之后整个自增变成原子操作。3.2 可见性JVM 内存语义规定解锁时把线程工作内存的修改全部刷新回主内存加锁时清空工作内存缓存强制从主内存读取最新值 因此 synchronized 天然保证可见性不需要额外搭配 volatile。3.3 有序性禁止指令重排序。编译器、CPU 不会随意打乱同步代码块内部代码的执行顺序保证多线程下逻辑符合预期。3.4 互斥性排他性同一把锁同一时间只能有一个线程拿到并进入同步代码其他竞争线程会阻塞等待这是锁最核心的能力。3.5 可重入性同一个线程已经拿到锁之后可以再次获取同一把锁不会自己把自己阻塞死。package thread; public class ReentrantDemo { // 锁对象this public synchronized void methodA(){ System.out.println(进入方法A,当前线程Thread.currentThread().getName()); // 在A里面调用同样加了synchronized的方法B methodB(); System.out.println(退出方法A,当前线程Thread.currentThread().getName()); } public synchronized void methodB(){ System.out.println(进入方法B,当前线程Thread.currentThread().getName()); System.out.println(退出方法B,当前线程Thread.currentThread().getName()); } public static void main(String[] args) { ReentrantDemo demo new ReentrantDemo(); new Thread(demo::methodA, 线程1).start(); } }运行结果进入方法A,当前线程线程1 进入方法B,当前线程线程1 退出方法B,当前线程线程1 退出方法A,当前线程线程1反推如果 synchronized 不可重入线程 1 拿到 this 锁进入 methodA调用 methodB 时会再次尝试抢 this 锁自己阻塞自己直接死锁。底层原理锁对象内部维护「持有线程标识 锁计数器」每重入一次计数器 1退出同步代码计数器 - 1计数器归零才真正释放锁。四、synchronized 的三种使用方式核心一句话锁是加在对象上的不是加在代码上所有用法本质都是锁定某个对象。4.1 修饰实例方法锁对象当前实例 thispublic synchronized void test(){ // 代码 }锁this当前对象实例规则同一个对象调用该方法互斥不同对象调用互不影响。4.2 修饰静态方法锁对象类的 Class 对象public static synchronized void test2(){ // 代码 }锁当前类的 Class 对象全局唯一规则所有该类的实例共用同一把锁只要调用静态同步方法就会互斥。⚠️ 高频面试坑实例同步方法锁 this和静态同步方法锁 class是两把不同的锁互不干扰不会互斥。4.3 修饰同步代码块手动指定锁对象最灵活synchronized (锁对象){ // 代码 }锁对象可以是任意 Object 对象、this、类名.class。 推荐写法只把需要保护的共享操作包进去缩小锁范围提高并发效率。记忆口诀实例方法锁 this静态方法锁 class代码块锁你括号里写的对象。五、synchronized 底层锁升级机制JDK1.6 对 synchronized 做了重大优化锁会根据竞争程度单向升级只能升不能降全程状态记录在对象头 Mark Word 中。升级路径无锁 → 偏向锁 → 轻量级锁自适应自旋 → 重量级锁5.1 偏向锁无竞争场景乐观锁适用只有一个线程反复获取锁几乎没有竞争原理第一个加锁的线程用 CAS 把自己的线程 ID 写入 Mark Word后续该线程再次进入同步块只对比线程 ID不需要 CAS开销极低。触发升级出现第二个线程竞争锁时偏向锁撤销升级为轻量级锁。本质延迟加锁无竞争就不做真正的加锁操作。5.2 轻量级锁少量竞争自旋 CAS乐观锁适用多线程交替使用锁竞争少、锁持有时间短原理线程在栈帧创建锁记录用 CAS 尝试把对象头指向自己的锁记录CAS 失败就进入自适应自旋循环重试抢锁。触发升级自旋多次仍失败、多个线程同时竞争 → 膨胀为重量级锁。优点用户态操作线程不挂起开销小缺点锁持有时间长时自旋空耗 CPU。5.3 重量级锁大量竞争悲观锁适用竞争激烈、锁持有时间长原理底层依赖操作系统mutex互斥锁抢锁失败的线程不再自旋直接被操作系统挂起进入阻塞队列让出 CPU锁释放后唤醒等待线程。优点不消耗 CPU 自旋缺点用户态内核态切换、线程调度开销大。5.4 JVM 额外优化锁消除逃逸分析发现锁对象只在单线程内使用不存在并发直接把加锁代码删掉。锁粗化连续多次加锁解锁比如循环里加锁JVM 自动合并成一次加锁一次解锁减少频繁操作开销。六、全文总结6.1 synchronized 锁策略全景对应结合开篇讲的通用锁策略回头再看 synchronized它是一套自适应的锁方案不同阶段对应不同锁策略乐观锁 → 悲观锁开始默认低冲突采用乐观锁策略锁冲突频繁后自动切换为悲观锁。轻量级锁 → 重量级锁初始是轻量级锁锁持有时间变长、竞争加剧单向膨胀为重量级锁。轻量级锁基于自旋锁轻量级锁阶段采用自适应自旋策略抢锁失败先循环重试不阻塞线程。非公平锁不保证先来后到锁释放后新线程和排队线程一起竞争吞吐量更高。可重入锁同一个线程可重复获取同一把锁靠计数器实现不会自己锁死自己。普通独占互斥锁不是读写锁不区分读写场景所有操作都互斥读多写少场景推荐使用专门的读写锁。6.2 核心结论三大并发特性原子性、可见性、有序性附加特性互斥、可重入、自动释放锁。三种使用方式实例方法锁 this、静态方法锁 class、同步代码块锁指定对象。锁升级单向不可逆无锁 → 偏向锁 → 轻量级锁 → 重量级锁低竞争靠 CAS 优化高竞争切内核态互斥锁。选型建议简单同步优先用synchronized需要超时、公平锁、精准唤醒选ReentrantLock简单计数优先用 CAS 原子类。