首页 > 技术文章 > 可重入锁ReentrantLock

lukazan 2021-05-18 21:14 原文

1、概述

(1)简介

        所谓的可重入是指,线程可对同一把锁进行重复加锁,而不会被阻塞住,这样可避免死锁的产生。ReentrantLock 的主要功能和 synchronized 关键字一致,均是用于多线程的同步。但除此之外,ReentrantLock 在功能上比 synchronized 更为丰富。比如 ReentrantLock 在加锁期间,可响应中断,可设置超时等。
        ReentrantLock 内部是基于 AbstractQueuedSynchronizer(以下简称AQS)实现的。

(2)常用API

  • getHoldCount():查询当前线程保持此锁的次数,也就是执行此线程执行lock方法的次数
  • getQueueLength():返回正等待获取此锁的线程估计数,比如启动10个线程,1个线程获得锁,此时返回的是9
  • hasQueuedThread(Thread thread):查询给定线程是否等待获取此锁
  • hasQueuedThreads():是否有线程等待此锁
  • isFair():该锁是否公平锁
  • isLock():此锁是否有任意线程占用
  • lockInterruptibly():响应中断,如果当前线程未被中断,获取锁
  • tryLock():尝试获得锁,仅在调用时锁未被线程占用,获得锁
  • tryLock(long timeout TimeUnit unit):如果锁在给定等待时间内没有被另一个线程保持,则获取该锁

(3)和Synchronized的异同

  比起synchronized功能更加丰富,支持公平锁实现,支持中断响应以及限时等待等等。可以配合一个或多个Condition条件方便的实现等待通知机制。
  • 阻塞等待
    • 使用synchronized锁:占有锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,那么其他线程就只能一直等待
    • 当ReetraintLock能够响应中断,限时等待,不用无限期的等待下去
  • 公平锁
    • Synchronized是非公平锁
    • ReetraintLock支持公平锁和非公平锁,默认是非公平锁
  • 实现层面
    • Synchronized是JVM层面实现的
    • ReetraintLock是JDK层面实现的
  • 释放锁:Synchronized自动、ReetraintLock手动
  • 读写线程
    • Synchronized是互斥锁。
    • 提供了ReentrantReadWriteLock来实现读共享,写独占。

2、原理

(1)可重入特性

        ReentrantLock 内部是通过 AQS 实现同步控制的,AQS 有一个变量 state 用于记录同步状态。初始情况下,state = 0,表示 ReentrantLock 目前处于解锁状态。如果有线程调用 lock 方法进行加锁,state 就由0变为1,如果该线程再次调用 lock 方法加锁,就让其自增,即 state++。线程每调用一次 unlock 方法释放锁,会让 state–。通过查询 state 的数值,即可知道 ReentrantLock 被重入的次数了。

(2)抽象队列同步器AbstractQueuedSynchronizer(AQS)

        在 AQS 内部,通过维护一个FIFO 队列来管理多线程的排队工作。在公平竞争的情况下,无法获取同步状态的线程将会被封装成一个节点,置于队列尾部。入队的线程将会通过自旋的方式获取同步状态,若在有限次的尝试后,仍未获取成功,线程则会被阻塞住。
 
        在同步队列中,头结点是获取了同步状态的节点。其他节点在尝试获取同步状态失败后,会被阻塞住,暂停运行。当头结点释放同步状态后,会唤醒其后继节点。后继节点会将自己设为头节点,并将原头节点从队列中移除。大致示意图如下:

(3)公平锁与非公平锁

1)介绍

  • 公平模式下,线程在同步队列中通过 FIFO 的方式获取锁,每个线程最终都能获取锁。
  • 在非公平模式下,线程会通过“插队”的方式去抢占锁,抢不到的则进入同步队列进行排队。
  • 默认情况下,ReentrantLock 使用的是非公平模式获取锁,而不是公平模式。不过我们也可通过 ReentrantLock 构造方法ReentrantLock(boolean fair)调整加锁的模式。

2)区别:

  • 公平模式下,可保证每个线程最终都能获得锁,但效率相对比较较低。
  • 非公平模式下,效率比较高,但可能会导致线程出现饥饿的情况。即一些线程迟迟得不到锁,每次即将到手的锁都有可能被其他线程抢了。

3)为什么非公平模式下效率更高?

        在激烈竞争的情况下,非公平锁的性能高于公平锁的性能的一个原因是:在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。假设线程 A 持有一个锁,并且线程 B 请求这个锁。由于这个线程已经被线程 A 持有,因此 B 将被挂起。当 A 释放锁时,B 将被唤醒,因此会再次尝试获取锁。与此同时,如果 C 也请求这个锁,那么 C 很有可能会在 B 被完全唤醒前获得、使用以及释放这个锁。这样的情况时一种“双赢”的局面:B 获得锁的时刻并没有推迟,C 更早的获得了锁,并且吞吐量也获得了提高。
        如上图,线程 C 在线程 B 苏醒阶段内获取和使用锁,并在线程 B 获取锁前释放了锁,所以线程 B 可以顺利获得锁。线程 C 在抢占锁的情况下,仍未影响线程 B 获取锁,因此是个“双赢”的局面。

4)使用场景

        如果线程持锁时间短,则应使用非公平锁,可通过“插队”提升效率。如果线程持锁时间长,“插队”带来的效率提升可能会比较小,此时应使用公平锁。

3、源码分析


        ReentrantLock的操作是基于Sync内部抽象类实现的,Sync是一个静态抽象类,继承了 AbstractQueuedSynchronizer。公平和非公平锁的实现类NonfairSync和FairSync则继承自 Sync 。至于 ReentrantLock 中的其他一些方法,主要逻辑基本上都在几个内部类中实现的。
        ReentrantLock 中获取锁方法的源码,由于获取锁的方式有公平和非公平之分,所以具体的实现是在NonfairSync和FairSync两个类中。
public void lock() {
    sync.lock();
}
 
abstract static class Sync extends AbstractQueuedSynchronizer {
    // 这里的 lock 是抽象方法,具体的实现在两个子类中
    abstract void lock();
    // 省略其他无关代码
}

(1)公平锁获取锁

  • 调用 acquire 方法,将线程放入同步队列中进行等待
  • 线程在同步队列中成功获取锁,则将自己设为持锁线程后返回
    • 若同步状态不为0,且当前线程为持锁线程,则执行重入逻辑
+--- ReentrantLock.FairSync.java
final void lock() {
    // 调用 AQS acquire 获取锁
    acquire(1);
}
 
+--- AbstractQueuedSynchronizer.java
/**
* 该方法主要做了三件事情:
* 1. 调用 tryAcquire 尝试获取锁,该方法需由 AQS 的继承类实现,获取成功直接返回
* 2. 若 tryAcquire 返回 false,则调用 addWaiter 方法,将当前线程封装成节点,并将节点放入同步队列尾部
* 3. 调用 acquireQueued 方法让同步队列中的节点循环尝试获取锁
*/
public final void acquire(int arg) {
    // acquireQueued 和 addWaiter 属于 AQS 中的方法,这里不展开分析了
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();   
}
 
+--- ReentrantLock.FairSync.java
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    // 获取同步状态
    int c = getState();
    // 如果同步状态 c 为0,表示锁暂时没被其他线程获取
    if (c == 0) {
        /*
         * 判断是否有其他线程等待的时间更长。如果有,应该先让等待时间更长的节点先获取锁。
         * 如果没有,调用 compareAndSetState 尝试设置同步状态。
         */
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            // 将当前线程设置为持有锁的线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 如果当前线程为持有锁的线程,则执行重入逻辑
    else if (current == getExclusiveOwnerThread()) {
        // 计算重入后的同步状态,acquires 一般为1
        int nextc = c + acquires;
        // 如果重入次数超过限制,这里会抛出异常
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        // 设置重入后的同步状态
        setState(nextc);
        return true;
    }
    return false;
}
 
+--- AbstractQueuedSynchronizer.java
/** 该方法用于判断同步队列中有比当前线程等待时间更长的线程 */
public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    /*
     * 在同步队列中,头结点是已经获取了锁的节点,头结点的后继节点则是即将获取锁的节点。
     * 如果有节点对应的线程等待的时间比当前线程长,则返回 true,否则返回 false
     */
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

(2)非公平锁获取锁

  • 调用 compareAndSetState 方法抢占式加锁,加锁成功则将自己设为持锁线程,并返回
  • 若加锁失败,则调用 acquire 方法,将线程置于同步队列尾部进行等待
  • 线程在同步队列中成功获取锁,则将自己设为持锁线程后返回
    • 若同步状态不为0,且当前线程为持锁线程,则执行重入逻辑
+--- ReentrantLock.NonfairSync
final void lock() {
    /*
     * 这里调用直接 CAS 设置 state 变量,如果设置成功,表明加锁成功。这里并没有像公平锁那样调用 acquire 方法让线程进入同步队列进行排队,而是直接调用 CAS 抢占锁。
     * 抢占失败再调用 acquire 方法将线程置于队列尾部排队。
     */
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}
 
+--- AbstractQueuedSynchronizer
/** 参考上一节的分析 */
public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
 
+--- ReentrantLock.NonfairSync
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
 
+--- ReentrantLock.Sync
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    // 获取同步状态
    int c = getState();
    
    // 如果同步状态 c = 0,表明锁当前没有线程获得,此时可加锁。
    if (c == 0) {
        // 调用 CAS 加锁,如果失败,则说明有其他线程在竞争获取锁
        if (compareAndSetState(0, acquires)) {
            // 设置当前线程为锁的持有线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 如果当前线程已经持有锁,此处条件为 true,表明线程需再次获取锁,也就是重入
    else if (current == getExclusiveOwnerThread()) {
        // 计算重入后的同步状态值,acquires 一般为1
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        // 设置新的同步状态值
        setState(nextc);
        return true;
    }
    return false;
}
(3)释放锁
+--- ReentrantLock
public void unlock() {
    // 调用 AQS 中的 release 方法
    sync.release(1);
}
 
+--- AbstractQueuedSynchronizer
public final boolean release(int arg) {
    // 调用 ReentrantLock.Sync 中的 tryRelease 尝试释放锁
    if (tryRelease(arg)) {
        Node h = head;
        /*
         * 如果头结点的等待状态不为0,则应该唤醒头结点的后继节点。
         * 这里简单说个结论:头结点的等待状态为0,表示头节点的后继节点线程还是活跃的,无需唤醒
         */
        if (h != null && h.waitStatus != 0)
            // 唤醒头结点的后继节点,该方法的分析请参考我写的关于 AQS 的文章
            unparkSuccessor(h);
        return true;
    }
    return false;
}
 
+--- ReentrantLock.Sync
protected final boolean tryRelease(int releases) {
    /*
     * 用同步状态量 state 减去释放量 releases,得到本次释放锁后的同步状态量。
     * 当将 state 为 0,锁才能被完全释放
     */
    int c = getState() - releases;
    // 检测当前线程是否已经持有锁,仅允许持有锁的线程执行锁释放逻辑
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
        
    boolean free = false;
    // 如果 c 为0,则表示完全释放锁了,此时将持锁线程设为 null
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    
    // 设置新的同步状态
    setState(c);
    return free;
}

(3)释放锁

+--- ReentrantLock
public void unlock() {
    // 调用 AQS 中的 release 方法
    sync.release(1);
}
 
+--- AbstractQueuedSynchronizer
public final boolean release(int arg) {
    // 调用 ReentrantLock.Sync 中的 tryRelease 尝试释放锁
    if (tryRelease(arg)) {
        Node h = head;
        /*
         * 如果头结点的等待状态不为0,则应该唤醒头结点的后继节点。
         * 这里简单说个结论:头结点的等待状态为0,表示头节点的后继节点线程还是活跃的,无需唤醒
         */
        if (h != null && h.waitStatus != 0)
            // 唤醒头结点的后继节点,该方法的分析请参考我写的关于 AQS 的文章
            unparkSuccessor(h);
        return true;
    }
    return false;
}
 
+--- ReentrantLock.Sync
protected final boolean tryRelease(int releases) {
    /*
     * 用同步状态量 state 减去释放量 releases,得到本次释放锁后的同步状态量。
     * 当将 state 为 0,锁才能被完全释放
     */
    int c = getState() - releases;
    // 检测当前线程是否已经持有锁,仅允许持有锁的线程执行锁释放逻辑
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
        
    boolean free = false;
    // 如果 c 为0,则表示完全释放锁了,此时将持锁线程设为 null
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    
    // 设置新的同步状态
    setState(c);
    return free;
}

 引用:http://www.tianxiaobo.com/2018/05/07/Java-%E9%87%8D%E5%85%A5%E9%94%81-ReentrantLock-%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90/

推荐阅读