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 → 退出 monitorexit3.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 核心三要素:
state:volatile int,表示锁状态(0=未占用,>0=重入次数)
CLH队列:双向链表,存储等待线程
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 对比
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现方式 | JVM内置 | Java API |
| 锁释放 | 自动(异常也释放) | 需在finally中unlock() |
| 可中断性 | 不支持 | lockInterruptibly() |
| 超时尝试 | 不支持 | tryLock(timeout) |
| 公平性 | 非公平 | 支持公平/非公平 |
| 条件等待 | wait/notify | Condition(多路通知) |
| 性能(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); }三大问题:
ABA问题→ 使用
AtomicStampedReference带版本号自旋开销→ 自适应自旋或退让
单变量限制→ 使用
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 |
| 高并发 Map | ConcurrentHashMap |
| 单变量原子更新 | 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、不可变对象)才是终极追求。