java.util.concurrent.Locks 使用指南

更新于 2025-12-29

baeldung 2025-03-26

1. 概述

简而言之,锁(Lock) 是一种比标准 synchronized 代码块更灵活、更强大的线程同步机制。

Lock 接口自 Java 1.5 起引入,位于 java.util.concurrent.locks 包中,提供了丰富的锁操作功能。

在本教程中,我们将探讨 Lock 接口的不同实现及其应用场景。


2. Lock 与 synchronized 块的区别

使用 synchronized 块和 Lock API 之间存在一些关键差异:

  • 作用范围不同synchronized 块必须完全包含在一个方法内;而 Locklock()unlock() 操作可以分别放在不同的方法中。
  • 公平性支持synchronized 不支持公平性——一旦锁被释放,任何线程都可能获取它,无法指定优先级。而 Lock API 可通过构造函数参数启用公平锁,确保等待时间最长的线程优先获得锁。
  • 非阻塞尝试:若线程无法获取 synchronized 锁,它将一直阻塞。而 Lock 提供了 tryLock() 方法,仅在锁可用且未被其他线程持有时才获取锁,从而减少线程等待时间。
  • 可中断性:处于“等待”状态以获取 synchronized 块访问权限的线程不能被中断。而 Lock API 提供了 lockInterruptibly() 方法,允许在等待锁时响应中断,并抛出 InterruptedException 异常。

3. Lock API

Lock 接口的主要方法如下:

  • void lock():获取锁。如果锁不可用,当前线程将被阻塞,直到锁被释放。
  • void lockInterruptibly():与 lock() 类似,但允许在等待锁的过程中响应中断,并抛出 InterruptedException
  • boolean tryLock():非阻塞版本的 lock()。立即尝试获取锁,成功则返回 true
  • boolean tryLock(long timeout, TimeUnit timeUnit):与 tryLock() 类似,但最多等待指定的超时时间后放弃。
  • void unlock():释放锁。

⚠️ 重要提示:已加锁的实例必须始终被解锁,否则可能导致死锁。

推荐使用以下结构来确保锁被正确释放:

Lock lock = ...; 
lock.lock();
try {
    // 访问共享资源
} finally {
    lock.unlock();
}

此外,除了 Lock 接口,还有 ReadWriteLock 接口,它维护一对锁:

  • 一个用于只读操作(允许多个线程同时读),
  • 一个用于写操作(独占)。

只要没有写操作正在进行,多个线程可以同时持有读锁。

ReadWriteLock 声明了两个方法:

  • Lock readLock():返回用于读操作的锁。
  • Lock writeLock():返回用于写操作的锁。

4. Lock 的实现类

4.1 ReentrantLock

ReentrantLock 实现了 Lock 接口,提供了与 synchronized 相同的并发性和内存语义,但功能更强大。

基本用法示例:

public class SharedObjectWithLock {
    ReentrantLock lock = new ReentrantLock();
    int counter = 0;

    public void perform() {
        lock.lock();
        try {
            // 临界区
            counter++;
        } finally {
            lock.unlock();
        }
    }
}

✅ 务必使用 try-finally 块包裹 lock()unlock(),防止死锁。

使用 tryLock() 示例:

public void performTryLock() {
    boolean isLockAcquired = lock.tryLock(1, TimeUnit.SECONDS);
    
    if (isLockAcquired) {
        try {
            // 临界区
        } finally {
            lock.unlock();
        }
    }
    // 若未获取到锁,可选择重试或执行其他逻辑
}

在此例中,调用 tryLock() 的线程最多等待 1 秒,若仍未获取锁则放弃。


4.2 ReentrantReadWriteLock

ReentrantReadWriteLock 实现了 ReadWriteLock 接口。

加锁规则:

  • 读锁:如果没有线程持有写锁或正在请求写锁,多个线程可同时获取读锁。
  • 写锁:只有在没有线程进行读或写操作时,一个线程才能获取写锁。

使用示例:

public class SynchronizedHashMapWithReadWriteLock {
    Map<String, String> syncHashMap = new HashMap<>();
    ReadWriteLock lock = new ReentrantReadWriteLock();
    Lock writeLock = lock.writeLock();

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

    public String remove(String key) {
        try {
            writeLock.lock();
            return syncHashMap.remove(key);
        } finally {
            writeLock.unlock();
        }
    }
}

对于读操作,使用读锁:

Lock readLock = lock.readLock();

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

public boolean containsKey(String key) {
    try {
        readLock.lock();
        return syncHashMap.containsKey(key);
    } finally {
        readLock.unlock();
    }
}

多个读线程可同时访问,只要没有写操作正在进行。


4.3 StampedLock(Java 8 引入)

StampedLock 也支持读写锁,但其加锁方法会返回一个 stamp(戳记),用于后续解锁或验证锁是否仍然有效。

基本用法:

public class StampedLockDemo {
    Map<String, String> map = new HashMap<>();
    private StampedLock lock = new StampedLock();

    public void put(String key, String value) {
        long stamp = lock.writeLock();
        try {
            map.put(key, value);
        } finally {
            lock.unlockWrite(stamp);
        }
    }

    public String get(String key) throws InterruptedException {
        long stamp = lock.readLock();
        try {
            return map.get(key);
        } finally {
            lock.unlockRead(stamp);
        }
    }
}

乐观读锁(Optimistic Reading)

StampedLock 还支持乐观读锁:大多数情况下,读操作无需等待写操作完成,因此不需要完整的读锁。

public String readWithOptimisticLock(String key) {
    long stamp = lock.tryOptimisticRead();
    String value = map.get(key);

    // 验证读期间是否有写操作发生
    if (!lock.validate(stamp)) {
        // 升级为悲观读锁
        stamp = lock.readLock();
        try {
            return map.get(key);
        } finally {
            lock.unlockRead(stamp);
        }
    }
    return value;
}

乐观读适用于读多写少、且读操作能容忍短暂不一致的场景,性能更高。


5. 条件变量(Condition)

Condition 类允许线程在执行临界区时等待某个条件成立

例如:一个读线程获取了共享队列的锁,但队列为空,无法消费数据,此时可等待“非空”条件。

传统 Java 使用 wait()notify()notifyAll() 实现线程通信,而 Condition 提供了更灵活的机制——可定义多个条件

示例:带容量限制的栈

public class ReentrantLockWithCondition {
    Stack<String> stack = new Stack<>();
    int CAPACITY = 5;

    ReentrantLock lock = new ReentrantLock();
    Condition stackEmptyCondition = lock.newCondition();   // 栈空条件
    Condition stackFullCondition = lock.newCondition();    // 栈满条件

    public void pushToStack(String item) {
        try {
            lock.lock();
            while (stack.size() == CAPACITY) {
                stackFullCondition.await(); // 等待栈不满
            }
            stack.push(item);
            stackEmptyCondition.signalAll(); // 通知消费者栈非空
        } finally {
            lock.unlock();
        }
    }

    public String popFromStack() {
        try {
            lock.lock();
            while (stack.size() == 0) {
                stackEmptyCondition.await(); // 等待栈非空
            }
            return stack.pop();
        } finally {
            stackFullCondition.signalAll(); // 通知生产者栈有空间
            lock.unlock();
        }
    }
}

每个 Condition 对象都与一个 Lock 关联,通过 lock.newCondition() 创建。


6. 总结

本文介绍了 java.util.concurrent.locks 包中 Lock 接口的多种实现:

  • ReentrantLock:基础可重入锁,功能强于 synchronized
  • ReentrantReadWriteLock:适用于读多写少的场景,提升并发性能。
  • StampedLock:Java 8 新增,支持乐观读,进一步优化读密集型应用。

此外,我们还探讨了如何使用 Condition 实现多条件的线程协作,替代传统的 wait/notify 机制,使线程通信更清晰、灵活。

合理使用这些工具,可显著提升多线程程序的性能与可控性。