Lock

Lock接口及其实现类:Java 并发编程的灵活锁机制

Lock 接口是 JDK 5 引入的同步工具,为 Java 并发编程提供了比 synchronized 更灵活的锁控制能力。它支持显式加锁 / 解锁、可中断获取、超时获取等特性,弥补了 synchronized 隐式操作的局限性。本文将深入解析 Lock 接口的核心实现类(ReentrantLockReentrantReadWriteLock),并对比其与 synchronized 的差异。

Lock 接口核心方法

Lock 接口定义了锁的基本操作,核心方法如下:

方法 作用描述
lock() 获取锁(阻塞式,直到获取成功)
lockInterruptibly() 可中断地获取锁(获取过程中若线程被中断,会抛出 InterruptedException
tryLock() 尝试非阻塞获取锁(成功返回 true,失败返回 false
tryLock(long timeout, TimeUnit unit) 超时获取锁(超时未获取则返回 false
unlock() 释放锁(必须在 finally 块中调用,避免锁泄漏)
newCondition() 创建条件对象,实现线程间通信(类似 synchronizedwait/notify

ReentrantLock:可重入独占锁

ReentrantLockLock 接口的最常用实现,提供与 synchronized 类似的独占锁功能,但支持更灵活的控制。其核心特性是可重入性(同一线程可多次获取锁)和公平 / 非公平模式

公平锁与非公平锁

ReentrantLock 允许通过构造函数指定锁的公平性:

// 非公平锁(默认)  
Lock lock = new ReentrantLock();  
// 公平锁  
Lock fairLock = new ReentrantLock(true);
(1)非公平锁(NonfairSync)
  • 获取逻辑:线程尝试直接获取锁(CAS 操作),失败后才进入等待队列;
  • 特点:可能存在线程饥饿(先请求的线程后获取锁),但吞吐量更高;
  • 适用场景:并发激烈但允许偶尔不公平的场景。
(2)公平锁(FairSync)
  • 获取逻辑:线程必须按请求顺序进入队列,只有队首线程能获取锁;
  • 特点:保证公平性,避免饥饿,但频繁的上下文切换导致吞吐量较低;
  • 适用场景:对公平性要求高的场景(如资源调度)。

核心源码解析

ReentrantLock 基于 AQS(AbstractQueuedSynchronizer)实现,内部通过 Sync 抽象类区分公平与非公平逻辑:

获取锁(lock() 方法)
// 非公平锁的 lock() 实现  
static final class NonfairSync extends Sync {  
    final void lock() {  
        // 尝试直接获取锁(CAS 将 state 从 0 设为 1)  
        if (compareAndSetState(0, 1))  
            setExclusiveOwnerThread(Thread.currentThread());  
        else  
            // 失败则进入 AQS 的 acquire 流程  
            acquire(1);  
    }  

    // 尝试获取锁(可重入逻辑)  
    protected final boolean tryAcquire(int acquires) {  
        return nonfairTryAcquire(acquires);  
    }  
}  

// 可重入逻辑  
final boolean nonfairTryAcquire(int acquires) {  
    final Thread current = Thread.currentThread();  
    int c = getState();  
    if (c == 0) { // 锁未被占用  
        if (compareAndSetState(0, acquires)) {  
            setExclusiveOwnerThread(current);  
            return true;  
        }  
    }  
    else if (current == getExclusiveOwnerThread()) { // 当前线程已持有锁(重入)  
        int nextc = c + acquires;  
        if (nextc < 0) throw new Error("Maximum lock count exceeded");  
        setState(nextc);  
        return true;  
    }  
    return false;  
}
释放锁(unlock() 方法)
public void unlock() {  
    sync.release(1);  
}  

// AQS 的 release 方法调用 tryRelease  
protected final boolean tryRelease(int releases) {  
    int c = getState() - releases;  
    if (Thread.currentThread() != getExclusiveOwnerThread())  
        throw new IllegalMonitorStateException();  
    boolean free = false;  
    if (c == 0) { // 完全释放锁  
        free = true;  
        setExclusiveOwnerThread(null);  
    }  
    setState(c); // 更新重入计数  
    return free;  
}

多条件等待(Condition)

ReentrantLock 可通过 newCondition() 创建多个条件对象,实现更精细的线程通信:

Lock lock = new ReentrantLock();  
Condition notEmpty = lock.newCondition(); // 非空条件  
Condition notFull = lock.newCondition();  // 非满条件  

// 生产者线程  
lock.lock();  
try {  
    while (queue.isFull()) {  
        notFull.await(); // 队列满时等待  
    }  
    queue.add(item);  
    notEmpty.signal(); // 通知消费者  
} finally {  
    lock.unlock();  
}  

// 消费者线程  
lock.lock();  
try {  
    while (queue.isEmpty()) {  
        notEmpty.await(); // 队列空时等待  
    }  
    Object item = queue.poll();  
    notFull.signal(); // 通知生产者  
} finally {  
    lock.unlock();  
}

优势:相比 synchronized 的单条件队列,Condition 支持多条件分离,减少不必要的唤醒。

ReentrantReadWriteLock:读写分离锁

ReentrantReadWriteLock 是为读多写少场景优化的锁,其核心是读写分离

  • 读锁(ReadLock):共享锁,允许多个线程同时读取;
  • 写锁(WriteLock):独占锁,同一时间仅允许一个线程写入。

核心特性

  • 互斥规则:读锁与写锁互斥,写锁与写锁互斥,读锁与读锁不互斥;
  • 可重入性:读锁和写锁均支持重入(读锁重入需当前线程未持有写锁);
  • 降级与升级:写锁可降级为读锁(先获取写锁,再获取读锁,最后释放写锁),但读锁不可升级为写锁(避免死锁)。

状态设计

ReentrantReadWriteLock 复用 AQS 的 state 变量,通过位分割存储读写状态:

  • 低 16 位:写锁的重入计数(exclusiveCount);
  • 高 16 位:读锁的持有计数(sharedCount)。
static final int SHARED_SHIFT = 16;  
static final int SHARED_UNIT = (1 << SHARED_SHIFT); // 65536  
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; // 65535  

// 计算读锁计数  
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }  
// 计算写锁计数  
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

读锁与写锁的获取与释放

读锁(ReadLock)
  • 获取:若无线程持有写锁,或持有写锁的是当前线程,则累加读锁计数;
  • 释放:递减读锁计数,直至为 0 时完全释放。
public void lock() {  
    sync.acquireShared(1);  
}  

// AQS 共享模式获取  
protected final int tryAcquireShared(int unused) {  
    Thread current = Thread.currentThread();  
    int c = getState();  
    // 若有写锁且持有者不是当前线程,获取失败  
    if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)  
        return -1;  
    int r = sharedCount(c);  
    // 尝试获取读锁(CAS 累加高 16 位)  
    if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {  
        // 记录读锁重入信息(略)  
        return 1;  
    }  
    // 复杂场景的重试逻辑(略)  
    return fullTryAcquireShared(current);  
}
写锁(WriteLock)
  • 获取:若无线程持有读锁或写锁,或当前线程已持有写锁,则累加写锁计数;
  • 释放:递减写锁计数,直至为 0 时完全释放。

Lock 与 synchronized 的对比

特性 Lock(ReentrantLock) synchronized
锁获取方式 显式(lock()/unlock() 隐式(代码块 / 方法修饰)
公平性 支持公平 / 非公平模式 仅非公平模式
可中断性 支持(lockInterruptibly() 不支持(中断后仍会获取锁)
超时获取 支持(tryLock(timeout) 不支持
条件等待 多条件(Condition 单条件(对象监视器)
性能 高并发下更优(减少上下文切换) JVM 优化后与 Lock 接近
适用场景 复杂同步逻辑(超时、中断、多条件) 简单同步场景

锁的分类与最佳实践

锁的分类

  • 按可重入性:可重入锁(synchronizedReentrantLock)与不可重入锁;
  • 按公平性:公平锁与非公平锁;
  • 按共享模式:独占锁(ReentrantLock、写锁)与共享锁(读锁、Semaphore)。

最佳实践

  • 优先使用 synchronized:简单场景下代码更简洁,JVM 优化更成熟;
  • 使用 ReentrantLock:需中断、超时或多条件等待时;
  • 使用 ReentrantReadWriteLock:读多写少场景(如缓存、配置中心);
  • 避免死锁:始终在 finally 块中释放锁,按顺序获取多把锁。