理解ReentrantLock

108 阅读4分钟

本文深入理解一下ReenTrantLock,翻译为重入锁,是lock接口的一个实现类,也是使用频率很高的一个同步组件,同时在并发容器中也有使用。支持重入性,同时支持公平锁和非公平锁。

简介

image-20220326205736172.png

  • Sync是AQS的一个子类
  • NonfairSync继承自Sync,为非公平锁
  • FairSync 继承自Sync ,公平锁

这三个类通过重写AQS中的方法和调用AQS提供的方法实现锁的获取和释放。并且主要重写的方法为TryAquire和TryRelease方法,也就是ReenTrantLock为独占式锁。

ReenTrantLock的重入

ReenTrantLock是独占式锁,AQS的state属性既可以表示同步状态也可以记录重入次数。因为ReenTrantLock获取锁的过程是互斥的,也就是同一时刻只有一个线程可以获取得到锁,那么可以通过AQS的state>0来表示同步状态,state的数值来表示重入次数。

获取锁

以下代码就是重入逻辑:

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()) {
		// 3. 再次获取,计数加一
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

测试: 以debug模式启动,断点打在最后一个lock()处:

public static void main(String[] args) {
    ReentrantLock lock = new ReentrantLock();
    new Thread(()->{
        try {
            lock.lock();
            lock.lock();
            lock.lock();
        }finally {
		//不释放锁
        }
    }).start();
}

image-20220326215800378.png state为2,也就是ReenTrantLock支持重入,重入逻辑就是同步状态值加一。


再看一下释放锁的逻辑:

当前同步状态减一,当同步状态为0,返回true,锁释放成功。

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;
}
对比Synchronized

我们知道Synchronized是通过monitorEnter和monitorExit两个指令来隐式的加锁和释放锁。执行monitorEnter指令前需要获取对象监视器monitor。Synchronized支持重入,线程执行monitorEnter指令计数器加一,monitorExit指令计数器减一,当计数器为0时锁就释放成功。

public static void main(String[] args) throws InterruptedException {
    //共享资源
    Object o = new Object();
    try {
        synchronized (o){
            System.out.println("首次获取锁,进入同步代码");
            synchronized (o){
                System.out.println("再次获取锁");
            }
        }
    }catch (Exception e){
        System.out.println("异常");
    }
}

用javap -v查看字节码文件 image-20220326223443185.png 后面的两个monitorExit指令,是遇到异常需要执行的指令。


公平锁和非公平锁

公平锁表现在'排队获取锁',针对获取锁的过程是公平的,即满足先进先出。非公平锁表现在竞争,谁有能耐谁获取锁,竞争存在于head节点释放锁时。

ReenTrantLock默认为非公平锁

 public ReentrantLock() {
     sync = new NonfairSync();
 }
 public ReentrantLock(boolean fair) {
     sync = fair ? new FairSync() : new NonfairSync();
 }

公平锁相比非公平锁只是多了一个hasQueuedPredecessors()来判断同步队列是否为存在节点的判断,具体看实现逻辑:

FairSync.tryAcquire方法:

protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (!hasQueuedPredecessors() &&
                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;
    }
}

这个方法判断的是当前同步队列是否还有正在排队节点,存在返回true、不存在返回false。

public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    //获取头结点和尾结点
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    //缓存
    Node s;
    //头结点尾结点不是一个节点,也就是完成了初始化 并且同步队列存在其他节点
    return h != t &&
        //头结点的后继节点为null   当前线程不是头结点后驱节点线程
        ((s = h.next) == null || s.thread != Thread.currentThread());
}
图示:

同步队列原状态:

image-20220401181641908.png 解释:sync为同步器、head和tail为头尾节点、exclusiveOwner为同步状态拥有者(这是AQS父类一个属性)、Node们构成同步队列。

公平锁情况下排队获取锁:

image-20220401183105531.png 公平锁情况下,会先判断同步队列列里是否还有节点,如果有则获取同步状态失败,尾插法将节点插入同步队列,并且使得sync的tail指向刚插入的节点。即公平锁在同步队列有节点的情况下不会进行cas竞争。

非公平锁情况下: 可能会出现这种情况,cas竞争 image-20220401183532598.png 当头结点完全释放锁时,如果此刻刚好启动一个线程去获取锁,同时此刻head节点的后驱节点也在cas自旋尝试获取得到锁,如果head节点的后驱节点cas失败了,也就出现上图所示状态,同步队列没有变化,但是锁却被一个同步队列外的线程获取,形成了插队!!!

公平锁 VS 非公平锁

  1. 公平锁每次获取到锁为同步队列中的第一个节点,保证请求资源时间上的绝对顺序,而非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,造成“饥饿”现象
  2. 公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量