Java随笔-锁
Java 中的锁可以从多个维度分类但核心演进脉络集中在 synchronized从 纯重量级锁到锁升级体系的优化以及 JUC 显式锁体系的成熟。下面按分类、演进、原理三层展开。一、Java 锁的宏观分类1. 按实现层面类型代表控制方隐式锁synchronizedJVM 自动管理进入/退出 monitorenter/monitorexit显式锁ReentrantLock、ReadWriteLock程序员手动lock()/unlock()2. 按竞争策略类型思想代表悲观锁先加锁再操作synchronized、ReentrantLock乐观锁先操作提交时 CAS 检查AtomicInteger、LongAdder3. 按特性公平/非公平是否按请求顺序分配锁可重入/不可重入同一线程能否多次获取同一把锁共享/独占读锁共享、写锁独占自旋/阻塞竞争时 CPU 空转等待 vs 线程挂起4. 按状态synchronized 核心演进的主线无锁 → 偏向锁 → 轻量级锁 → 重量级锁二、锁的演进史synchronized 的四代优化1. JDK 1.0 ~ 1.5纯重量级锁性能黑洞早期 synchronized 直接调用操作系统mutex互斥量线程阻塞 用户态 → 内核态切换线程唤醒 内核态 → 用户态切换即使单线程反复进入也要走完整内核流程当时 Java 并发性能被严重诟病直到 Doug Lea 推出 JUCjava.util.concurrent 包用 ReentrantLock CAS 绕过 JVM 锁。2. JDK 1.6锁升级体系JVM 团队反击JVM 团队发现大多数锁不仅不存在多线程竞争而且总是由同一线程多次获得。于是引入 锁升级Lock Coarsening/Elimination 之外的核心优化① 无锁Lock-Free对象初始状态或显式使用 Atomic 类依赖 CPU 的 cmpxchgCAS指令Mark Word 标志…| age:4 | biased_lock:0 | lock:01② 偏向锁Biased Locking场景只有一个线程访问同步块原理第一次获取锁时用 CAS 把线程 ID 写入对象头的 Mark Word。之后该线程进入同步块只需检查 Mark Word 里的线程 ID 是否是自己无需任何原子操作。撤销成本一旦有第二个线程尝试获取必须在安全点STW撤销偏向锁这个代价不小。现状JDK 15 废弃JEP 374JDK 18 默认关闭。现代硬件上收益变小撤销成本反而成为负担。③ 轻量级锁Lightweight Locking场景多线程交替执行竞争不激烈没有同时抢原理线程在自己的栈帧中创建 Lock Record然后用 CAS 把对象头的 Mark Word 替换为指向该 Lock Record 的指针。成功获得锁失败自旋默认 10 次JDK 1.6 后引入自适应自旋根据历史成功率动态调整次数优点线程不阻塞避免内核态切换缺点自旋消耗 CPU竞争激烈时自旋就是浪费④ 重量级锁Heavyweight Locking场景竞争激烈自旋失败原理锁膨胀inflate为 MonitorObjectMonitor对象头 Mark Word 指向 ObjectMonitor 地址未获得锁的线程进入 _EntryList 阻塞持有锁的线程执行完后从 _EntryList 唤醒一个竞争线程开销内核态切换但适合长时间持有锁的场景3. JDK 1.6 另外两项编译优化优化原理效果锁消除Lock Elimination逃逸分析发现锁对象不会被其他线程访问直接去掉同步局部变量StringBuffer的同步常被消除锁粗化Lock Coarsening相邻的同步块合并为一个更大的同步块减少反复加锁/解锁开销三、Mark Word 结构锁状态的载体对象头中的Mark Word是锁状态的存储载体。以64 位 JVM为例锁状态62 bitsbiased_lock (1 bit)lock (2 bits)无锁unused:25 hash:31 unused:1 age:4001偏向锁thread:54 epoch:2 unused:1 age:41101轻量级锁ptr_to_lock_record:62-00重量级锁ptr_to_monitor:62-10GC 标记ptr_to_cms_promoted_obj:62-11lock 字段2 bits biased_lock 字段1 bit共同标识当前锁状态。四、锁升级完整流程图对象创建 │ ▼ ┌─────────────┐ │ 无锁状态 │◄────────────────────────┐ │ (匿名可偏向) │ │ └──────┬──────┘ │ │ │ ▼ │ 线程A尝试获取 │ │ │ ▼ │ ┌─────────────┐ 线程A再次进入 │ │ 偏向锁状态 │◄──────────────────┐ │ │ Mark Word记录 │ │ │ │ 线程A ID │ │ │ └──────┬──────┘ │ │ │ │ │ ▼ 线程B尝试获取 │ │ 检查epoch/撤销 │ │ │ │ │ ▼ │ │ ┌─────────────┐ │ │ │ 轻量级锁 │ CAS成功 │ │ │ 自旋竞争 │───────────────────┘ │ └──────┬──────┘ │ │ │ ▼ 自旋失败/竞争激烈 │ 锁膨胀inflate │ │ │ ▼ │ ┌─────────────┐ 锁释放后 │ │ 重量级锁 │────────────────────────┘ │ ObjectMonitor│ └─────────────┘关键规则锁升级是单向的偏向锁 → 轻量级锁 → 重量级锁不能降级重量级锁释放后对象回到无锁状态下次竞争重新走升级流程偏向锁撤销后可能直接升级为轻量级锁也可能先回到无锁五、显式锁体系JUC 的补足synchronized 的升级体系是 JVM 自动优化的但灵活性不足。JUC 提供了更精细的控制1. ReentrantLock基于 AQSReentrantLocklocknewReentrantLock(true);// true公平锁lock.lock();try{// 临界区}finally{lock.unlock();}底层是 AQSAbstractQueuedSynchronizer通过 volatile int state CAS 双向链表管理队列支持公平/非公平、可中断lockInterruptibly、超时tryLock(timeout)、多条件变量Condition2. ReadWriteLock读锁共享锁多个线程可同时读写锁独占锁写时阻塞读写适合读多写少场景3. StampedLockJDK 8引入乐观读锁先读提交时验证版本号未被写则成功被写了再升级为悲观读性能比 ReadWriteLock 更高但使用复杂六、对比总结维度synchronizedReentrantLockCAS无锁实现层JVM字节码monitorenterAPIAQSCPU 指令锁升级✅ 支持偏向→轻量→重量❌ 直接 AQS 队列无锁无升级公平性非公平可选公平/非公平-可中断❌ 不支持✅lockInterruptibly-条件队列1 个wait/notify多个Condition-性能JDK 1.6 后接近 ReentrantLock略优极端竞争无竞争时最优使用难度低自动释放高需finally unlock高ABA 问题七、演进时间线版本锁相关演进JDK 1.0synchronized纯重量级锁性能差JDK 1.2引入Lock接口雏形但 synchronized 未优化JDK 1.5JUC 包正式发布ReentrantLock、Atomic、AQSJDK 1.6锁升级偏向/轻量/重量、锁消除、锁粗化、自适应自旋JDK 1.7G1 GC继续优化同步性能JDK 1.8引入StampedLock、LongAdder分段 CASJDK 15废弃偏向锁JEP 374现代并发模型下收益为负JDK 18默认关闭偏向锁未来可能彻底移除八、总结Java 锁的演进本质是能不动内核就不动内核从早期直接调用操作系统 mutex重量级到发现大多数锁无竞争后引入偏向锁零开销、轻量级锁CAS 自旋再到 JUC 用 AQS 把排队逻辑搬到用户态。最终趋势是偏向锁被淘汰轻量级锁 CAS AQS成为主流。