1

图解ReentrantLock底层公平锁和非公平锁实现原理 - 朱季谦

 1 year ago
source link: https://www.cnblogs.com/zhujiqian/p/16898222.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

图解ReentrantLock底层公平锁和非公平锁实现原理

image

💻在面试或者日常开发当中,经常会遇到公平锁和非公平锁的概念。

两者最大的区别如下👇

1️⃣ 公平锁:N个线程去申请锁时,会按照先后顺序进入一个队列当中去排队,依次按照先后顺序获取锁。就像下图描述的上厕所的场景一样,先来的先占用厕所,后来的只能老老实实排队。

image

2️⃣ 非公平锁:N个线程去申请锁,会直接去竞争锁,若能获取锁就直接占有,获取不到锁,再进入队列排队顺序等待获取锁。同样以排队上厕所打比分,这时候,后来的线程会先尝试插队看看能否抢占到厕所,若能插队抢占成功,就能使用厕所,若失败就得老老实实去队伍后面排队。

image

针对这两个概念,我们通过ReentrantLock底层源码来分析下💁 :公平锁和非公平锁在ReentrantLock类当中锁怎样实现的。

🌈ReentrantLock内部实现的公平锁类是FairSync,非公平锁类是NonfairSync。

当ReentrantLock以无参构造器创建对象时,默认生成的是非公平锁对象NonfairSync,只有带参且参数为true的情况下FairSync,才会生成公平锁,若传参为false时,生成的依然是非公平锁,两者构造器源码结果如下👇

image

在实际开发当中,关于ReentrantLock的使用案例,一般是这个格式👇

 class X {    
   private final ReentrantLock lock = new ReentrantLock();    
   // ...      
   public void m() {      
     lock.lock();  
     // block until condition holds      
     try {        
       // ... method body      
     } finally {        
       lock.unlock()      
     }    
   }  
 }

这时的lock指向的其实是NonfairSync对象,即非公平锁。

当使用lock.lock()对临界区进行占锁操作时,最终会调用到NonfairSync对象的lock()方法。根据图1可知,NonfairSync和FairSync两者的lock方法实现逻辑是不一样的,而体现其锁是否符合公平与否的地方,就是在两者的lock方法里。

image

可以看到,在非公平锁NonfairSync的上锁lock方法当中,若if(compareAndSetState(0,1))判断不满足,就会执行acquire(1)方法,该方法跟公平锁FairSync的lock方法里调用的acquire(1)其实是同一个,但方法里的tryAcquire具体实现又存在略微不同,这里后面会讨论。

这里就呼应前文提到的非公平锁的概念——当N个线程去申请非公平锁,它们会直接去竞争锁,若能获取锁就直接占有,获取不到锁,再进入队列排队顺序等待获取锁。这里的“获取不到锁,再进入队列排队顺序等待获取锁”可以理解成⏩——当线程过来直接竞争锁失败后,就会变成公平锁的形式,进入到一个队列当中,按照先后顺序排队去获取锁。

而if(compareAndSetState(0,1))语句块的逻辑,恰好就体现了“当N个线程去申请非公平锁,它们会直接去竞争锁,若能获取锁就直接占有”这句话的意思。

🌈首先,先来分析NonfairSync的lock()方法原理,源码如下👇

final void lock() {
  //先竞争锁,若能竞争成功,则占有锁资源
    if (compareAndSetState(0, 1))
      //将独占线程成员变量设置为当前线程,表示占有锁资源的线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

compareAndSetState(0, 1)就是一个当前线程与其他线程抢占锁的过程,这里面涉及到AQS的知识点,因此,阅读本文时,需具备一定的AQS基础。

JUC的锁实现是基于AQS实现的,可以简单理解成,AQS里定义了一个private volatile int state变量,若state值为0,说明无线程占有,其他线程可以进行抢占锁资源;若state值为1,说明已有线程占有锁资源,其他线程需要等待该占有锁的线程释放锁资源后,方能进行抢占锁的动作。

image

线程在抢占锁时,是通过CAS对state变量进行置换操作的,期望值expect是0,更新值update为1,若期望值expect能与内存地址里的state值一致,就可以通过原子操作将内存地址里state值置换成更新值update,返回true,反之,就置换失败返回false。

protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

可见,这里的if (compareAndSetState(0, 1))就体现了非公平锁的机制,当前线程会先去竞争锁,若能竞争成功,就占有锁资源。

若竞争锁失败话,就会执行acquire(1)方法,其原理就相当走跟公平锁类似的逻辑。

acquire(1);

进入acquire方法,该方法是位于AbstractQueuedSynchronizer里,就是前文提到的AQS,即抽象同步队列器,它相当提供一套用户态层面的锁框架,基于它可以实现用户态层面的锁机制。

public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

注意一点,NonfairSync和FairSync调用的acquire(int arg)方法中的tryAcquire方法,其实现是不同的。NonfairSync调用的acquire方法,其底层tryAcquire调用的是NonfairSync重写的tryAcquire方法;FairSync调用的acquire方法,其底层tryAcquire调用的是FairSync重写的tryAcquire方法。

image

NonfairSync类的acquire方法的流程图如下👇

image

先分析非公平锁的!tryAcquire(arg)底层源码实现,该方法的整体逻辑是,通过getState()获取state状态值,判断是否已为0。若state等于0了,说明此时锁资源处于无锁状态,那么,当前线程就可以直接再执行一遍CAS原子抢锁操作,若CAS成功,说明已成功抢占锁。若state不为0,再判断当前线程是否与占有资源的锁为同一个线程,若同一个线程,那么就进行重入锁操作,即ReentrantLock支持同一个线程对资源的重复加锁,每次加锁,就对state值加1,解锁时,就对state解锁,直至减到0最后释放锁。

🌈最后,若在该方法里,通过CAS抢占锁成功或者重入锁成功,那么就会返回true,若失败,就会返回false。

final boolean nonfairTryAcquire(int acquires) {
    //获取当前线程引用
    final Thread current = Thread.currentThread();
    //获取AQS的state状态值
    int c = getState();
    //若state等于0了,说明锁处于无被占用状态,可被当前线程抢占
    if (c == 0) {
        //再次尝试通过CAS抢锁
        if (compareAndSetState(0, acquires)) {
            //将独占线程成员变量设置为当前线程,表示占有锁资源的线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //判断当前线程是否与占有锁资源的线程为同一个线程
    else if (current == getExclusiveOwnerThread()) {
      //每次重入锁,state就会加1  
      int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

在 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))代码当中,根据 &&短路机制,若!tryAcquire(arg)为false,就不会再执行后面代码。反之,若!tryAcquire(arg)为true,说明抢占锁失败了或者不属于重入锁,那么就会继续后续acquireQueued(addWaiter(Node.EXCLUSIVE), arg))代码的执行。acquireQueued里面的逻辑,就可以理解成“获取不到锁,再进入队列排队顺序等待获取锁”。这块内容涉及比较复杂的双向链表逻辑,我后面会另外写一篇文章深入分析,本文主要是讲解公平锁和非公平锁的区别科普。

FairSync公平锁lock方法里acquire(1)的逻辑与非公平锁NonfairSync的acquire(1)很类似,其底层实现同样是这样👇

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

我们来看下FairSync类里重实现的tryAcquire与NonfairSync最终执行的tryAcquire区别👇

image

可以看到,公平锁FairSync的tryAcquire方法比NonfairSync的nonfairTryAcquire方法多了一行!hasQueuedPredecessors()代码。

在FairSync公平锁里,若hasQueuedPredecessors()返回false,那么!hasQueuedPredecessors()就会为true,在执行以下判断时,就会通过compareAndSetState(0, acquires)即CAS原子抢占锁。

if (!hasQueuedPredecessors() &&
    compareAndSetState(0, acquires)) {
    setExclusiveOwnerThread(current);
    return true;
}

那么,什么情况下,hasQueuedPredecessors() 能得到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 &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

❗存在两种情况:

1️⃣ 第一种情况,h != t为false,说明head和tail节点都为null或者h和t都指向一个假节点head,这两种情况都说明了,此时的同步队列还没有初始化,简单点理解,就是在当前线程之前,还没有出现线程去抢占锁,因此,此时,锁是空闲的, 同时当前线程算上最早到来的线程之一(高并发场景下同一时刻可能存在N个线程同时到来),就可以通过CAS竞争锁。

2️⃣ 第二种情况,h != t为true但(s = h.next) == null || s.thread != Thread.currentThread()为false,当头节点head和尾节点都不为空且指向不是同一节点,就说明同步队列已经初始化,此时至少存在两个以上节点,那么head.next节点必定不为空,即(s = h.next) == null会为false,若s.thread != Thread.currentThread()为false,说明假节点head的next节点刚好与当前线程为同一节点,也就意味着,当前线程排在队列最前面,排在前面的可以在锁空闲时获取锁资源,就可以执行compareAndSetState(0, acquires)去抢占锁资源。

若同步队列已经初始化,且当前线程又不是在假节点head的next节点,就只能老老实实去后面排队等待获取锁了。

image

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK