问答题1019/1053ReentrantLock实现原理

难度:
2021-11-02 创建

参考答案:

ReentrantLock 是 Java 并发包中的一种显式锁实现,它提供了比 synchronized 更强大的功能,能够灵活地控制线程的加锁和释放锁。它是 java.util.concurrent.locks 包中的一部分,常用于实现线程同步控制,支持公平性和重入等特性。

1. ReentrantLock 的基本概念

ReentrantLock 是一种可重入的锁,即同一个线程在获得锁后,可以多次进入该锁而不发生死锁。这种特性使得 ReentrantLock 能够支持递归调用的场景。ReentrantLockLock 接口的实现,它的主要优点是:

  • 可重入性:同一个线程可以多次获取同一个锁。
  • 公平性:可以选择公平锁或非公平锁,公平锁保证按请求顺序获取锁,非公平锁则可能会抢占其他线程的锁。
  • 中断响应:线程在等待锁时可以响应中断。
  • 定时锁:支持设定超时时间的锁。

2. ReentrantLock 的实现原理

ReentrantLock 的实现主要依赖于 java.util.concurrent.locks.AbstractQueuedSynchronizer(AQS)类。AQS 是一个提供了先进先出的同步队列的抽象类,它为实现各种同步器(如锁、信号量等)提供了通用的框架。

ReentrantLock 基于 AQS 来实现线程同步。它通过 AQS 提供的 acquirerelease 方法来控制锁的获取和释放。

2.1 锁的状态

ReentrantLock 内部通过一个 int 类型的变量 state 来记录锁的状态。state 的值主要表示两个方面的信息:

  • 锁是否被占用

  • 锁的重入次数(即同一个线程获取锁的次数)

  • state 为 0 时,锁是空闲的。

  • state 大于 0 时,锁被占用,state 的值表示当前线程重入锁的次数。

2.2 锁的获取(lock() 方法)

当调用 ReentrantLocklock() 方法时,实际执行的是 AQSacquire() 方法,流程如下:

  • 当前线程不是持有锁的线程
    • 如果 state 等于 0,表示锁没有被占用,当前线程可以通过 compareAndSetState 操作将 state 设置为 1,即占用锁,并将当前线程设置为持有锁的线程。
    • 如果 state 大于 0,表示锁已经被其他线程占用,当前线程会进入 AQS 的等待队列中,等待锁被释放。具体来说,线程会尝试通过 acquireQueued 方法进入队列等待,并在适当的时候重新尝试获取锁。
  • 当前线程已经是持有锁的线程
    • 如果当前线程已经持有该锁,ReentrantLock 会通过增加 state 的值来支持重入锁操作(即允许当前线程多次调用 lock() 方法)。
1public void lock() { 2 if (Thread.currentThread() == getOwner()) { 3 state++; 4 return; 5 } 6 // 非公平锁的获取 7 if (!tryAcquire(state)) { 8 enqueue(Thread.currentThread()); 9 } 10}

2.3 锁的释放(unlock() 方法)

当调用 ReentrantLockunlock() 方法时,实际执行的是 AQSrelease() 方法,流程如下:

  • 当前线程会尝试将 state 减 1。
  • 如果 state 为 0,表示该线程已完全释放了锁,它会调用 release 方法来通知其他线程并唤醒等待队列中的线程。
  • 如果 state 大于 0,表示当前线程仍然持有锁,其他线程无法获取锁。
1public void unlock() { 2 if (Thread.currentThread() != getOwner()) { 3 throw new IllegalMonitorStateException(); 4 } 5 state--; 6 if (state == 0) { 7 setOwner(null); 8 // 唤醒等待队列中的下一个线程 9 release(); 10 } 11}

2.4 公平锁与非公平锁

ReentrantLock 提供了公平锁和非公平锁的选择:

  • 公平锁:在公平锁模式下,线程会按照请求的顺序来获取锁(即先到先得)。如果一个线程在等待队列中,它会按照进入的顺序尝试获取锁。
  • 非公平锁:在非公平锁模式下,线程可以在队列中的任何位置抢占锁,通常是最后一个线程会抢占锁。非公平锁通常具有更高的性能,因为它避免了线程的排队等待。

在创建 ReentrantLock 时,通过传入一个布尔值来指定是否启用公平锁:

1ReentrantLock lock = new ReentrantLock(true); // 公平锁 2ReentrantLock lock = new ReentrantLock(false); // 非公平锁

2.5 AQS 的工作机制

AQS 是 ReentrantLock 内部实现的核心类。它通过一个同步队列来管理线程的等待和唤醒。每个线程在获取锁时,如果锁不可用,就会被挂起,并加入到 AQS 的队列中,等待被唤醒后重新尝试获取锁。

  • AQS 状态:AQS 使用一个 state 来记录当前的同步状态。在 ReentrantLock 中,state 表示锁的占用情况。
  • 同步队列:AQS 使用一个双端队列来管理等待的线程,每个等待的线程都会作为节点加入队列。
  • 线程唤醒:当锁释放时,AQS 会唤醒队列中的线程,使其重新尝试获取锁。

AQS 的实现依赖于 acquirerelease 方法来控制线程的加锁和释放。当线程释放锁时,如果有其他线程等待锁,则会唤醒它们,并重新争夺锁。

3. ReentrantLock 的优缺点

优点:

  • 可重入性ReentrantLock 支持线程的重入性,允许同一线程多次获得锁,而不会发生死锁。
  • 公平性:可以选择公平锁,保证线程按请求顺序获取锁,避免线程饥饿。
  • 中断响应ReentrantLock 支持响应中断,线程可以在等待锁时被中断,从而避免长时间阻塞。
  • 定时锁:支持定时锁,可以设置超时时间,防止线程长时间无法获取锁。
  • 条件变量ReentrantLock 提供了 Condition 机制,可以更灵活地控制线程的同步。

缺点:

  • 比 synchronized 更复杂ReentrantLock 的实现比 synchronized 更复杂,使用时需要手动释放锁,否则会导致死锁。
  • 性能开销:虽然 ReentrantLocksynchronized 提供了更多功能,但由于额外的复杂性,它的性能可能比 synchronized 略低。

4. 使用 ReentrantLock 示例

1public class ReentrantLockExample { 2 private final ReentrantLock lock = new ReentrantLock(); 3 4 public void criticalSection() { 5 lock.lock(); 6 try { 7 // 执行临界区代码 8 System.out.println("In critical section"); 9 } finally { 10 lock.unlock(); 11 } 12 } 13}

最近更新时间:2024-12-06