Skip to content

ReentrantLock

1. ReentrantLock简介

ReentrantLock意思是"可重入锁",可理解为可以重复使用的锁,ReentrantLock是实现了Lock接口的类,并且ReentrantLock提供了更多的方法。ReentrantLock还提供了公平锁也非公平锁的选择,构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。
公平锁与非公平锁的区别在于公平锁的锁获取是有顺序的,因而非公平锁的效率更高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。

2. ReentrantLock特性

  1. 可重入性: 如果一个线程已经持有了某个锁,那么它可以再次调用lock()方法而不会被阻塞。锁的持有计数会在每次成功调用lock()方法时递增,并在每次unlock()方法被调用时递减。
  2. 公平性: ReentrantLock提供了一个公平锁的选项。公平锁会按照线程请求锁的顺序来分配锁,而不是像非公平锁那样允许线程抢占已经等待的线程的锁。公平锁可以减少“饥饿”的情况,但也可能降低一些性能。
  3. 可中断性:ReentrantLock的获取锁操作(lockInterruptibly()方法)可以被中断。这提供了另一个相对于synchronized关键字的优势,因为synchronized不支持响应中断。
  4. 条件变量: ReentrantLock类中还包含一个Condition接口的实现,该接口允许线程在某些条件下等待await()或唤醒signal()

3. ReentrantLock的使用

java
public class Demo_ReentrantLock {

    static class Ticketer {
        private int num = 30;
        // 创建可重入锁
        private ReentrantLock lock = new ReentrantLock();
        public void sale() {
            // 上锁
            lock.lock();
            try {
                if (num > 0) {
                    System.out.println(Thread.currentThread().getName() + "卖了1张, 还剩" + --num);
                }
            }finally {
                // 解锁
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        Ticketer ticketer = new Ticketer();
        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticketer.sale();
            }
        }, "AA").start();
        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticketer.sale();
            }
        }, "BB").start();
        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                ticketer.sale();
            }
        }, "CC").start();
    }
}

运行结果: Alt text 线程调用start()不一定马上执行,可能立即执行可能隔一会儿执行。
查看start()方法源码:

java
public synchronized void start() {
    
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
        }
    }
}

private native void start0();

start0()是一个本地方法,操作系统完成线程的创建,已经超出JVM的管理范围, 所以线程的启动时间不确定。

4. ReentrantLock的实现原理

ReentrantLock的实现依赖于内部的Sync类,这个类是AbstractQueuedSynchronizer(AQS)的一个实现。AQS是Java并发库中许多同步工具(包括Semaphore、CountDownLatch和CyclicBarrier等)的核心。
AQS使用一个int类型的变量来表示同步状态,ReentrantLock用它来表示锁的持有计数和持有线程的信息。当计数为0时,表示锁未被任何线程持有。当一个线程首次成功获取锁时,JVM会记录这个锁的持有线程,并将计数器设置为1。如果同一个线程再次请求这个锁,它将能够再次获得这个锁,并且计数器会递增。当线程释放锁时(通过调用unlock()方法),计数器会递减。如果计数器递减为0,则表示锁已经完全释放,其他等待的线程有机会获取它。
Alt text
AQS内部维护着一个FIFO双向队列,该队列就是CLH同步队列。AQS依赖它来完成同步状态的管理,当前线程如果获取同步状态失败(就是图中加锁没成功)时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。公平锁与非公平锁的区别在于获取锁的时候是否按照AQS的队列FIFO的顺序来。