JDK Locks 设计及工作原理与教程

一、锁设计的哲学:为什么需要锁?

在多线程环境下,竞态条件(Race Condition)是万恶之源。

锁的本质是提供一种内存可见性操作原子性的保证。

核心矛盾:性能 vs. 安全。锁的设计就是在吞吐量和数据一致性之间寻找平衡点。

二、JDK 锁体系架构全景图

├── 悲观锁(阻塞式) │ ├── synchronized(JVM内置锁) │ └── ReentrantLock(API级锁) │ ├── 公平/非公平模式 │ └── 可中断/超时机制 ├── 乐观锁(非阻塞式) │ └── CAS + AtomicXXX(硬件级支持) └── 读写锁分离 ├── ReentrantReadWriteLock └── StampedLock(JDK 8+,乐观读)

三、synchronized:JVM 的"亲儿子"

3.1 底层原理

// 代码层面 public synchronized void method() { ... } // 字节码层面 // ACC_SYNCHRONIZED 标志位 → 进入 monitorenter → 退出 monitorexit

3.2 锁升级过程(JDK 1.6 优化)

无锁 → 偏向锁 → 轻量级锁(CAS自旋) → 重量级锁(OS互斥量)

关键设计思想

  • 偏向锁:一个线程反复获取锁,消除同步开销(默认延迟4秒启用)

  • 轻量级锁:多线程交替执行,使用CAS自旋,避免线程阻塞

  • 重量级锁:竞争激烈时,挂起线程,让出CPU

3.3 实战示例

public class SynchronizedDemo { private int counter = 0; // 修饰实例方法:锁是当前实例对象 public synchronized void increment() { counter++; } // 修饰静态方法:锁是Class对象 public static synchronized void staticMethod() { // ... } // 同步代码块:更细粒度控制 public void blockLock() { Object lock = new Object(); synchronized (lock) { // 临界区代码 } } }

四、ReentrantLock:API 级的全能选手

4.1 核心设计架构

ReentrantLock └── Sync(继承AbstractQueuedSynchronizer) ├── NonfairSync(非公平,默认) └── FairSync(公平)

AQS 核心三要素

  1. state:volatile int,表示锁状态(0=未占用,>0=重入次数)

  2. CLH队列:双向链表,存储等待线程

  3. CAS操作:原子更新state

4.2 关键源码解析

// 非公平锁的 lock() 实现 final void lock() { // 上来先抢一次,不管队列(非公平核心) if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); // 尝试获取失败则入队 } // acquire 流程(模板方法模式) public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }

4.3 与 synchronized 对比

特性synchronizedReentrantLock
实现方式JVM内置Java API
锁释放自动(异常也释放)需在finally中unlock()
可中断性不支持lockInterruptibly()
超时尝试不支持tryLock(timeout)
公平性非公平支持公平/非公平
条件等待wait/notifyCondition(多路通知)
性能(JDK 1.6+)接近(已优化)相近

4.4 标准使用模式

public class ReentrantLockDemo { private final ReentrantLock lock = new ReentrantLock(true); // 公平锁 private final Condition condition = lock.newCondition(); private int count = 0; public void doWork() { lock.lock(); try { // 业务逻辑 count++; condition.signalAll(); // 唤醒等待线程 } finally { lock.unlock(); // 必须释放! } } public void waitForCondition() throws InterruptedException { lock.lockInterruptibly(); // 可中断 try { while (count < 10) { condition.await(); // 释放锁并等待 } } finally { lock.unlock(); } } }

五、读写锁:读读并发,读写互斥

5.1 ReentrantReadWriteLock 设计

public class ReadWriteLockDemo { private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); private final Lock readLock = rwl.readLock(); private final Lock writeLock = rwl.writeLock(); private Map<String, Object> cache = new HashMap<>(); // 读操作:多线程可并发 public Object get(String key) { readLock.lock(); try { return cache.get(key); } finally { readLock.unlock(); } } // 写操作:独占 public void put(String key, Object value) { writeLock.lock(); try { cache.put(key, value); } finally { writeLock.unlock(); } } }

注意陷阱:锁降级(写锁降级为读锁)可行,但锁升级(读锁升级为写锁)会导致死锁!

5.2 StampedLock:乐观读优化(JDK 8+)

public class StampedLockDemo { private final StampedLock sl = new StampedLock(); private int x = 0, y = 0; // 乐观读:无锁读取,验证后决定是否重试 public int distanceFromOrigin() { long stamp = sl.tryOptimisticRead(); int curX = x, curY = y; if (!sl.validate(stamp)) { // 被修改了,升级为悲观读 stamp = sl.readLock(); try { curX = x; curY = y; } finally { sl.unlockRead(stamp); } } return (int) Math.sqrt(curX * curX + curY * curY); } }

适用场景:读多写少,且读操作可容忍短暂不一致。


六、CAS:无锁并发基石

6.1 原理

// 模拟 CAS 操作(实际由硬件指令 cmpxchg 支持) public final boolean compareAndSet(int expect, int update) { // 如果当前值 == expect,则更新为 update // 否则返回 false return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }

三大问题

  1. ABA问题→ 使用AtomicStampedReference带版本号

  2. 自旋开销→ 自适应自旋或退让

  3. 单变量限制→ 使用AtomicReference封装多个变量

6.2 原子类实战

// 基本类型 AtomicInteger atomicInt = new AtomicInteger(0); atomicInt.incrementAndGet(); // ++i // 引用类型 AtomicReference<Student> ref = new AtomicReference<>(); ref.compareAndSet(old, new); // 累加器(高吞吐量,但弱一致性) LongAdder adder = new LongAdder(); adder.increment(); long sum = adder.sum(); // 最终一致性

七、锁优化最佳实践

7.1 锁粒度控制

// ❌ 坏:锁住整个方法 public synchronized void bad() { // 1. 耗时计算(不需要同步) // 2. 少量共享数据操作 } // ✅ 好:只锁临界区 public void good() { // 耗时计算 synchronized (this) { // 只同步必要部分 } }

7.2 锁分离策略

// 使用 ThreadLocal 避免锁 ThreadLocal<SimpleDateFormat> tl = ThreadLocal.withInitial(SimpleDateFormat::new); // 使用 ConcurrentHashMap 分段锁(JDK 1.7) // JDK 1.8 改为 CAS + synchronized 优化

7.3 死锁预防

// 破坏循环等待:统一锁顺序 public void transfer(Account from, Account to, int amount) { // 按 hash 值排序,确保获取锁顺序一致 int fromHash = System.identityHashCode(from); int toHash = System.identityHashCode(to); if (fromHash < toHash) { synchronized (from) { synchronized (to) { /* ... */ } } } else { synchronized (to) { synchronized (from) { /* ... */ } } } }

7.4 性能对比建议

场景推荐方案
竞争不激烈,代码简单synchronized(JVM优化)
需要公平锁/中断/超时ReentrantLock
读多写少(缓存)StampedLock(乐观读)或ReadWriteLock
计数器累加LongAdder
高并发 MapConcurrentHashMap
单变量原子更新AtomicXXX

八、面试高频问题

Q1:synchronized 和 ReentrantLock 性能谁好?
JDK 1.6 后两者性能相近。synchronized 在低竞争时有偏向锁优化,ReentrantLock 在中高竞争时可中断等待,更灵活。

Q2:AQS 中的 state 有什么作用?
表示锁状态,独占模式下 0=空闲,>0=重入次数;共享模式下表示剩余资源数。

Q3:公平锁为什么性能比非公平锁差?
公平锁需要检查等待队列,且上下文切换频繁。非公平锁允许插队,减少了线程挂起/唤醒开销。

Q4:锁的可重入性如何实现?
在持有锁的线程再次获取时,判断当前持有线程是否为自身,是则 state+1,释放时 state-1。


九、总结:锁选型决策树

是否需要可中断/超时? ├─ 是 → ReentrantLock └─ 否 → 竞争是否激烈? ├─ 低 → synchronized(JVM帮你优化) └─ 高 → 读多写少? ├─ 是 → StampedLock(乐观读) └─ 否 → ReentrantLock + Condition

记住:锁的最佳实践不是"用什么锁",而是"能不能不用锁"。无锁化(CAS、ThreadLocal、不可变对象)才是终极追求。