Chapter 03

锁机制

从 synchronized 内置锁到 ReentrantLock、ReadWriteLock、StampedLock,掌握 Java 完整锁体系

锁的本质

锁(Lock)是保证原子性和可见性最直接的手段。其核心语义是:同一时刻,只允许一个线程执行被保护的代码段。 Java 提供了两类锁:

内置锁(Intrinsic Lock)
即 synchronized 关键字使用的锁,也叫监视器锁(Monitor Lock)。每个 Java 对象都有一个关联的监视器。语言级别支持,语法简洁,JVM 自动优化(偏向锁、轻量级锁升级)。
显式锁(Explicit Lock)
java.util.concurrent.locks 包下的锁,如 ReentrantLock、ReadWriteLock、StampedLock。功能更强大(超时、可中断、公平性、多条件变量),但需要手动加锁/解锁。

synchronized 内置锁

三种使用形式

public class SyncDemo {
    private int count = 0;
    private static int staticCount = 0;

    // 形式1:同步实例方法 —— 锁的是 this 对象
    public synchronized void increment() {
        count++;
    }

    // 形式2:同步静态方法 —— 锁的是 SyncDemo.class 对象
    public static synchronized void incrementStatic() {
        staticCount++;
    }

    // 形式3:同步代码块 —— 锁的是指定对象,粒度更细
    public void doWork() {
        // 非同步代码(并发执行)
        int localResult = expensiveCompute();

        synchronized (this) {  // 只同步必要的部分
            count += localResult;
        }
    }

    private int expensiveCompute() { return 1; }
}

对象头与 Mark Word

JVM 中每个对象都有一个对象头(Object Header),包含两个机器字:

Mark Word
32/64 位,存储哈希码、GC 分代年龄、锁状态标志、指向轻量级锁的指针或指向 Monitor 的指针。锁升级的核心数据结构。
Klass Pointer
指向对象的类元数据,JVM 通过它确认对象是哪个类的实例。

锁升级:偏向锁 → 轻量级锁 → 重量级锁

Java 6 引入了锁升级机制(自适应锁),根据竞争激烈程度逐步升级,避免在低竞争场景下进入 开销大的重量级锁:

无锁 │ │ 第一个线程获取锁(CAS 写入 Mark Word) ▼ 偏向锁(Biased Lock) │ 记录线程 ID,再次进入时无需任何操作(零成本) │ 有第二个线程尝试获取 → 撤销偏向锁 ▼ 轻量级锁(Lightweight Lock) │ 通过 CAS 自旋尝试获取(不挂起 OS 线程) │ 自旋超过阈值或有第三个线程等待 → 膨胀 ▼ 重量级锁(Heavyweight Lock / Monitor) 线程进入阻塞队列(OS 级别挂起) 释放锁时唤醒等待线程 开销最大(内核态切换)
Java 15 移除偏向锁

Java 15(JEP 374)默认禁用偏向锁,Java 21 正式废弃。原因是现代应用的锁竞争模式与偏向锁的设计假设不符, 维护偏向锁撤销逻辑反而增加了 JVM 复杂度。高度竞争的现代应用直接用轻量级锁+重量级锁即可。

wait() / notify() / notifyAll()

这三个方法用于线程间的协调,必须在 synchronized 块中调用(否则抛出 IllegalMonitorStateException):

// 经典生产者-消费者模式
public class BoundedBuffer<T> {
    private final Queue<T> queue = new LinkedList<>();
    private final int capacity;

    public BoundedBuffer(int capacity) { this.capacity = capacity; }

    public synchronized void put(T item) throws InterruptedException {
        while (queue.size() == capacity) {  // 用 while,不用 if(防止虚假唤醒)
            wait(); // 释放锁并等待,直到被 notify
        }
        queue.add(item);
        notifyAll(); // 唤醒所有等待的消费者线程
    }

    public synchronized T take() throws InterruptedException {
        while (queue.isEmpty()) {
            wait(); // 队列空时等待
        }
        T item = queue.poll();
        notifyAll(); // 唤醒等待的生产者线程
        return item;
    }
}
必须用 while 而非 if

等待条件判断必须用 while 循环,原因:(1) 虚假唤醒(Spurious Wakeup)—— 线程可能在没有被 notify 的情况下意外醒来(OS 底层允许); (2) 多个线程竞争时,被唤醒的线程重新获得锁后条件可能已经改变。 while 循环确保每次唤醒后都重新检查条件。

ReentrantLock

基本使用

ReentrantLock 是 synchronized 的显式替代,提供相同的互斥语义,但功能更丰富:

import java.util.concurrent.locks.*;

public class Counter {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock();           // 加锁
        try {
            count++;
        } finally {
            lock.unlock();     // 必须在 finally 中解锁!
        }
    }

    // 可中断加锁:等待期间可被 interrupt() 取消
    public void incrementInterruptibly() throws InterruptedException {
        lock.lockInterruptibly();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    // 非阻塞尝试加锁:立即返回 true/false
    public boolean tryIncrement() {
        if (lock.tryLock()) {
            try {
                count++;
                return true;
            } finally {
                lock.unlock();
            }
        }
        return false; // 锁被占用,立即返回
    }

    // 超时尝试加锁
    public boolean tryIncrementWithTimeout() throws InterruptedException {
        if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
            try {
                count++;
                return true;
            } finally {
                lock.unlock();
            }
        }
        return false; // 100ms 内未获得锁
    }
}

公平锁 vs 非公平锁

// 公平锁:按等待队列顺序分配锁,先到先得
ReentrantLock fairLock = new ReentrantLock(true);

// 非公平锁(默认):允许新到来的线程「插队」
ReentrantLock unfairLock = new ReentrantLock(false); // 或 new ReentrantLock()
公平锁
保证等待最久的线程优先获得锁,避免饥饿(Starvation)。但性能较差,因为每次都需要检查等待队列,并唤醒挂起线程(涉及上下文切换)。
非公平锁(默认)
新线程直接尝试 CAS 抢锁,不排队。吞吐量更高(减少线程切换),但可能导致某些线程长时间等待(饥饿)。大多数场景推荐非公平锁。

Condition:比 wait/notify 更灵活的条件变量

public class BetterBoundedBuffer<T> {
    private final ReentrantLock lock = new ReentrantLock();
    // 两个条件变量(相当于两个等待集合),精确唤醒
    private final Condition notFull  = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    private final Object[] items;
    private int head, tail, count;

    public BetterBoundedBuffer(int capacity) { items = new Object[capacity]; }

    public void put(T x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length) notFull.await();  // 只有生产者等待
            items[tail] = x;
            if (++tail == items.length) tail = 0;
            ++count;
            notEmpty.signal(); // 只唤醒消费者(精确),不唤醒生产者
        } finally { lock.unlock(); }
    }

    @SuppressWarnings("unchecked")
    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) notEmpty.await(); // 只有消费者等待
            T x = (T) items[head];
            items[head] = null;
            if (++head == items.length) head = 0;
            --count;
            notFull.signal(); // 只唤醒生产者(精确)
            return x;
        } finally { lock.unlock(); }
    }
}

ReadWriteLock 读写锁

ReadWriteLock 允许多个线程同时读(读-读不互斥),但写操作必须独占(读-写、写-写互斥)。 适合「读多写少」的场景,可以显著提升并发读的吞吐量。

读写锁的互斥矩阵: 读锁(共享) 写锁(独占) 读锁(共享) ✓ 兼容 ✗ 互斥 写锁(独占) ✗ 互斥 ✗ 互斥
public class RWCache<K, V> {
    private final Map<K, V> cache = new HashMap<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock  = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();

    public V get(K key) {
        readLock.lock(); // 共享锁:允许多个线程同时读
        try {
            return cache.get(key);
        } finally {
            readLock.unlock();
        }
    }

    public void put(K key, V value) {
        writeLock.lock(); // 独占锁:写时排斥所有读写
        try {
            cache.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }
}
写锁饥饿问题

在读请求非常频繁时,写线程可能长时间等待(写锁饥饿)。 ReentrantReadWriteLock(true)(公平模式)可以缓解此问题, 但性能会下降。如果读写比例不够悬殊,反而不如直接用 ReentrantLock

StampedLock(Java 8+)

StampedLock 是 Java 8 引入的更高性能的读写锁,增加了乐观读(Optimistic Read)模式, 允许读操作在不加任何锁的情况下执行,通过版本戳(stamp)检验数据是否被修改。

写锁(Write Lock)
独占锁,与 ReadWriteLock 的写锁类似。调用 writeLock() 返回 stamp,解锁时传入 stamp。
悲观读锁(Pessimistic Read Lock)
共享锁,调用 readLock(),与 ReadWriteLock 的读锁类似。
乐观读(Optimistic Read)
调用 tryOptimisticRead() 获得一个版本戳,不加任何锁直接读取数据,读完后调用 validate(stamp) 验证数据是否被写操作修改。若验证失败,升级为悲观读锁重试。
import java.util.concurrent.locks.*;

public class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();

    public void move(double deltaX, double deltaY) {
        long stamp = sl.writeLock(); // 写锁
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp);
        }
    }

    // 乐观读:最理想路径(无锁,高吞吐)
    public double distanceFromOrigin() {
        long stamp = sl.tryOptimisticRead(); // 获取版本戳,不加锁
        double currentX = x, currentY = y;   // 读取数据(可能被并发写修改)

        if (!sl.validate(stamp)) {           // 检查读取期间是否有写操作
            // 验证失败:有写操作发生,升级为悲观读锁重新读
            stamp = sl.readLock();
            try {
                currentX = x;
                currentY = y;
            } finally {
                sl.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
}
StampedLock 的局限

锁的选择指南

场景 推荐锁 理由
简单互斥,代码简洁优先 synchronized JVM 自动释放,无法忘记 unlock
需要可中断/超时/公平 ReentrantLock 显式锁提供更多控制选项
多条件变量(精确唤醒) ReentrantLock + Condition 比 wait/notify 更精确
读多写少(读:写 > 10:1) StampedLock 乐观读无锁,吞吐最高
读多写少(中等读写比) ReentrantReadWriteLock 功能完整,支持可重入
原子计数、CAS 操作 AtomicXxx / LongAdder 无锁算法,性能最优

死锁的预防

死锁(Deadlock)发生在多个线程循环等待对方持有的锁时。预防死锁的策略:

// 死锁预防:使用 tryLock + 有序加锁
public static void transfer(Account from, Account to, double amount)
        throws InterruptedException {
    // 固定锁顺序:按账户 ID 排序,避免循环等待
    Account first  = from.getId() < to.getId() ? from : to;
    Account second = from.getId() < to.getId() ? to   : from;

    boolean acquired = false;
    while (!acquired) {
        if (first.lock.tryLock(10, TimeUnit.MILLISECONDS)) {
            try {
                if (second.lock.tryLock(10, TimeUnit.MILLISECONDS)) {
                    try {
                        from.debit(amount);
                        to.credit(amount);
                        acquired = true;
                    } finally { second.lock.unlock(); }
                }
            } finally { first.lock.unlock(); }
        }
    }
}

本章小结