enyang
enyang
Published on 2026-01-12 / 5 Visits
0
0

Java 多线程<5>——Lock 体系与并发控制

1 synchronized 边界

在使用 synchronized 的过程中,开发者往往会逐渐遇到几个无法绕开的问题。

第一,线程在等待锁的过程中无法被中断。一旦线程进入 BLOCKED 状态,除非获得锁,否则无法响应中断信号,这在需要快速取消任务的系统中是不可接受的。

第二,无法限制等待时间。synchronized 要么获取成功,要么无限期等待,这使得线程在高竞争环境下容易堆积,最终拖垮系统。

第三,条件同步能力不足。所有 wait 线程共享同一个隐式条件队列,notify 无法精确控制唤醒对象,复杂条件下代码迅速失控。

Lock 体系并不是为了“替代” synchronized,而是为了突破这些设计边界。

2 Lock 接口的抽象含义

2.1 Lock 是一种并发协议

Lock 的本质并不是一把锁,而是一套线程协作协议。

它明确规定了三件事情:

  • 线程如何尝试进入临界区

  • 失败时线程处于什么状态

  • 线程如何有序地离开临界区

这一点决定了 Lock 必须由代码显式控制,而不能由编译器隐式插入。

2.2 Lock 的基本使用模式

所有 Lock 实现都遵循同一套使用模式:

Lock lock = new ReentrantLock();

lock.lock();
try {
    // 临界区
} finally {
    lock.unlock();
}

这段结构不是习惯问题,而是语义问题。

unlock 放在 finally 中,意味着无论临界区发生什么异常,锁都必须被释放,否则后续线程将永久阻塞。

2.3 显式锁带来的风险

显式锁的灵活性是以风险为代价的,最常见的问题包括:

  • 忘记 unlock

  • 重复 unlock

  • 在未持有锁的情况下调用 unlock

  • 锁粒度过大导致性能退化

因此,Lock 更适合经验较多、对并发行为有明确预期的场景。

3 ReentrantLock 的深入分析

3.1 可重入锁的真实价值

可重入锁解决的不是“能不能加锁”的问题,而是“调用路径是否安全”的问题。

class Service {
    private final Lock lock = new ReentrantLock();

    public void methodA() {
        lock.lock();
        try {
            methodB();
        } finally {
            lock.unlock();
        }
    }

    public void methodB() {
        lock.lock();
        try {
            // 操作共享资源
        } finally {
            lock.unlock();
        }
    }
}

如果锁不可重入,methodA 在调用 methodB 时将直接死锁。

ReentrantLock 内部通过线程标识和重入计数,避免了这一问题。

3.2 公平锁与非公平锁的运行差异

Lock fairLock = new ReentrantLock(true);
Lock unfairLock = new ReentrantLock(false);

非公平锁在 lock 时,会先尝试一次 CAS 抢占锁,即使队列中已有等待线程。

这种“插队”行为减少了线程切换次数,在高并发下具有明显的性能优势。

公平锁则严格按照队列顺序获取锁,线程调度更加可预测,但吞吐量更低。

在绝大多数业务系统中,非公平锁是更合理的默认选择。

3.3 tryLock 的工程意义

if (lock.tryLock()) {
    try {
        // 获取成功
    } finally {
        lock.unlock();
    }
} else {
    // 获取失败,走降级逻辑
}

tryLock 使并发控制从“阻塞模型”转变为“分支模型”。

常见应用包括:

  • 避免线程堆积

  • 实现快速失败

  • 实现多资源尝试锁定

3.4 可中断锁的使用场景

try {
    lock.lockInterruptibly();
    try {
        // 临界区
    } finally {
        lock.unlock();
    }
} catch (InterruptedException e) {
    // 响应中断
}

lockInterruptibly 的意义在于:
线程在等待锁的过程中可以被中断,而不是只能被动等待。

这在以下场景中非常关键:

  • 线程池任务取消

  • 服务快速停机

  • 超时控制链路

4 Condition 的完整条件同步模型

4.1 wait/notify 的结构性问题

在 synchronized 模型下:

synchronized (obj) {
    while (!condition) {
        obj.wait();
    }
}

所有 wait 的线程都进入同一个等待集合,notify 唤醒的是随机线程。

当条件复杂时,大量线程被错误唤醒,又立即进入等待,造成性能浪费。

4.2 使用 Condition 拆分业务条件

Lock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();

每个 Condition 表示一种业务状态,等待线程只会被相关信号唤醒。

4.3 生产者消费者完整示例

class BoundedQueue {
    private final Object[] items = new Object[10];
    private int count, putIndex, takeIndex;

    private final Lock lock = new ReentrantLock();
    private final Condition notEmpty = lock.newCondition();
    private final Condition notFull = lock.newCondition();

    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length) {
                notFull.await();
            }
            items[putIndex] = x;
            putIndex = (putIndex + 1) % items.length;
            count++;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await();
            }
            Object x = items[takeIndex];
            takeIndex = (takeIndex + 1) % items.length;
            count--;
            notFull.signal();
            return x;
        } finally {
            lock.unlock();
        }
    }
}

这里的关键点不在代码本身,而在于:

  • 条件队列按业务语义拆分

  • signal 精确唤醒需要继续执行的线程

  • 避免无效唤醒和无意义竞争

5 ReadWriteLock 的并发控制细节

5.1 读写锁的并发模型

ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();

读锁是共享的,只要没有写锁,多个读线程可以并发执行。

写锁是独占的,获取写锁时会阻塞所有读写线程。

5.2 典型读写场景示例

class Cache {
    private final Map<String, String> map = new HashMap<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public String get(String key) {
        rwLock.readLock().lock();
        try {
            return map.get(key);
        } finally {
            rwLock.readLock().unlock();
        }
    }

    public void put(String key, String value) {
        rwLock.writeLock().lock();
        try {
            map.put(key, value);
        } finally {
            rwLock.writeLock().unlock();
        }
    }
}

这种结构在读多写少的场景下,可以显著提升并发吞吐量。

5.3 锁升级被禁止的原因

假设两个线程同时持有读锁,并尝试升级为写锁:

  • 线程 A 等待线程 B 释放读锁

  • 线程 B 等待线程 A 释放读锁

这是典型的死锁场景,因此读写锁从设计上禁止锁升级。

6 Lock 的底层实现逻辑

6.1 AQS 的状态竞争模型

AQS 使用一个 volatile 整型变量表示同步状态,所有线程通过 CAS 操作竞争该状态。

成功修改状态的线程获得锁,失败的线程进入同步队列等待。

6.2 同步队列与条件队列的流转

  • 获取锁失败进入同步队列

  • await 进入条件队列

  • signal 将线程转移回同步队列

线程在这两个队列之间的切换,是 Lock 体系支持复杂同步的基础。

7 Lock 与 synchronized 的选择

7.1 必须使用 Lock 的场景

  • 需要超时或可中断获取锁

  • 需要多个条件队列

  • 读写分离

  • 高并发且逻辑复杂

7.2 synchronized 更合适的情况

  • 简单互斥

  • 临界区短

  • 并发风险低

  • 更强调代码稳定性


Comment