ReentrantLock reentrantLock = new ReentrantLock();
new Thread(() -> {
reentrantLock.lock();
try {
TimeUnit.SECONDS.sleep(5);
reentrantLock.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
reentrantLock.lock();
}).start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
reentrantLock.lock();
}).start();
TimeUnit.DAYS.sleep(1);
简单的模拟程序
断点打进ReentrantLock的lock方法
-
默认是Nonfair非公平锁
往下走
这里是NonfairSync的lock方法,可以看到,这个方法的思路就是一个if else,使用CAS去设置状态为1,如果设置成功把AQS中的独占线程设为当前线程,如果设置成功lock方法直接就结束了。
-
第一次lock直接成功,现在端点到第二次lock,跳过之前分析过的代码断点直接到acquire方法
acquire方法做了几件事,先调用tryAcquire,如果返回false,则调用addWaiter方法创建一个独占Node节点,然后入队,tryAcquireAQS中没有具体实现,需要子类自己实现,往下走
进入到了Sync的nonfairTryAcquire方法中,首先是获取当前AQS中的state值,state是volatile修饰的,具有内存可见性,如果是0,则会再次尝试一次CAS设置,设置成功方法直接结束了。这里也是非公平锁跟公平锁的一个区别,非公平锁会在这里直接去争夺state的修改权,而公平锁必须是要自己是当前队列中第一个排队的才会去争抢修改权。
-
如果state不是0或者CAS设置失败,会再次判断当前线程是否是AQS里的独占线程,如果是的话 ,增加state的值,最后返回true,后面的逻辑也不会执行。这里可以看出来ReentrantLock是可重入锁,独占线程重复调用lock方法不会阻塞 ,跳出方法,继续往下走
这里进入到了AQS的addWaiter方法之中,首先将当前线程对象封装进Node节点中,Node节点有前驱后继指针,并且保存Thread对象,这里可以看出AQS其实维护了一个双向链表。
然后获取当前AQS中的tail对象,并且把新的node前驱指向当前的tail,然后使用CAS把当前的tail指向新的node,如果CAS设置成功,在把老的tail的next指向新的node,这样就完成了一次添加尾节点的操作。如果CAS设置失败或者tail为空,则进入enq方法,由于是第一次没有抢占到锁,tail为空,这里直接会去进去enq方法。
-
这里有一个细节就是这里其实是第二次lock方法,第一次线程成功lock住之后并没有初始化队列,只是修改状态并且设置独占线程,只有第二个线程来了之后才会初始化队列,这样也省去了不必要的队列初始化操作,由此可见AQS中的队列是等待队列,获取state修改权的线程不在队列中。
这里面可以看到是一个循环里面套着CAS操作, 是典型的自旋操作。如果当前tail仍然为空,尝试CAS设置head复制一个新的空Node对象,设置成功,tail也指向这个新的head,然后再次尝试加入尾部,这样就完成了一次队列的初始化操作,也就是说一个刚刚初始化完成的AQS队列有2个节点,head是一个空的node对象,tail是新加入的node对象。
这里其实有些细节需要注意,队列初始化的时候head其实是一个空节点,第一个等待线程处于第二个节点中,可以理解为第一个空节点是一个虚拟节点,也可以把它想象成是当前队列的独占线程,只不过因为独占线程不需要等待,所以节点中的thread对象为null。还有一个原因是因为等待线程进行park的时候需要先修改前驱节点的值,所以第一个等待线程需要前面有一个节点,至于为什么要修改前驱节点的值,这是为了多进行一次自旋操作,细节在后面可以看到。
-
至此可以大概总结一下addWaiter方法里面做的事情:
1.如果tail不为空,使用CAS尝试将当前新的Node加到AQS双向链表的尾部
2.如果tail为空或者CAS加入尾部失败。进去enq方法,enq方法是一个自旋操作,直到完成了新Node加入尾部或者初始化之后才会退出
addWaiter方法返回新加入的node节点,然后进入AQS的acquireQueued方法
首先查看方法注释:获取已经在队列中的独占并不可打断的线程, 用于条件阻塞方法以及获取。
首先查看for循环里面的内容, 这里又是一个自旋。 首先获取当前node对象的前驱节点,如果当前对象的前驱节点是head则再次尝试修改状态,这里可以看到AQS的等待队列中,只有node的前驱节点是head才会尝试继续争夺状态修改权,而node节点前驱是head代表当前Node是第一个等待节点,因为head是一个虚拟节点。-
往下走,进入shouldParkAfterFailedAcquire方法
-
首先查看参数,pred是node的前驱节点。首先判断前驱节点的watiStatus字段是不是为Node.SIGNAL,SIGNAL状态标识线程需要被释放。当前的pred是新创建的head节点,是一个空的对象,不会进入这个if里面,会走到下面的compareAndSetWaitStatus方法,这个方法其实就是将pred的waitStatus设置为Node.SIGNAL状态的,到这里暂时没有看出这个方法的作用,继续往下走
回到了acquireQueued方法,shouldParkAfterFailedAcquire返回了false,然后会重复执行循环体。注意,这里其实就已经完成了一次自旋操作,现在是进行第二次自旋,也就是说当线程第一次进入acquireQueued方法中,至少要自旋2次,自旋2次也就意味着调用了2次tryAcquire方法。
-
这里可以大胆猜测一下,自旋两次是为了提高性能,减少park的可能性,因为park需要阻塞线程,阻塞线程需要内核的调用,涉及用户态内核态切换,无非是降低性能的操作。
-
这时断点打到第二次自旋,此时再次获取当前对象的前置节点,当前对象仍然是刚才的head,不同的是head的waitStatus变成了-1,也就是Node.SIGNAL。此时仍然会再次尝试修改状态,失败后又进入到了shouldParkAfterFailedAcquire方法,这次我们再次进入
-
由于上一次shouldParkAfterFailedAcquire方法将pred的ws改成了Node.SIGNAL,这一次方法直接返回了true,然后会进去AQS的parkAndCheckInterrupt方法
这里终于到了核心的异步,阻塞当前线程。至此,我们总结一下acquireQueued方法:
1.首先是一个大的死循环,循环里面获取当前node节点的前驱节点,前驱节点是head会尝试修改state(子类实现,公平锁这里不会修改)
2.如果不是head或者修改失败会进入shouldParkAfterFailedAcquire方法,如果前驱节点是Node.SIGNAL状态方法返回true,然后后面直接会阻塞当前线程,如果不是Node.SIGNAL状态就CAS修改成Node.SIGNAL。所以此时node的前驱节点是Node.SIGNAL状态,而node是0状态。-
这时候尝试思考一下,如果后面又有一个线程去lock,新的node的加入到尾部,新的node的前驱node的waitStatus仍然是0, shouldParkAfterFailedAcquire方法一定会返回false,只有第二次进入该方法才会返回true,也就是说,每次新加入一个线程等待之前,都要修改前一个节点的waitStatus,以此来达到2次自旋操作,这里也是AQS设计的一个细节。 第二次自旋争抢失败后 会被阻塞,至此完成了一个线程的等待队列,值得注意的是head节点的thread属性是空。
为了方便理解画图解释一下:
-
接下来分析一下unLock方法。
-
这里走到了AQS的release方法中。这里先调用tryRelease,如果返回true,后面会进行等待队列的唤醒操作。tryRelease仍然是需要子类进行实现的,往下走
走到了Sync的tryRelease方法。这里首先获取当前state,然后减去1的值如果是0就会把当前的独占线程重新设为空,整个方法返回true,然后后面才能进行等待队列的唤醒。由此可见,ReentrantLock类lock了几次就得unLock几次
-
回到之前的release方法
获取当前的head节点,如果当前head节点waitStatus不为0进行唤醒,由之前的分析可以知道, head的waitStatus为-1代表队列中有线程在等待,head的waitStatus为0有可能是后面的节点正在进行自旋操作,还没有把head的状态改为-1。 进入unparkSuccessor方法
unparkSuccessor传进来的参数是head节点,这里使用CAS 将head的状态重置为0,但是失败并没有任何影响。为什么这里要将node的waitStatus改为0呢,其实由之前的分析可以知道,因为node的后继节点有可能正在自旋调用shouldParkAfterFailedAcquire方法将自己的waitStatus改为-1,如果这里将成功将节点重置为0,那么后继节点线程会多进行一次自旋。随后判断head的后继节点是否为空或者是大于0,大于0是CANCELLED状态,是不可唤醒的,如果出现其中之一情况会从后往前遍历等待队列,获取到node后的第一个waitStatus小于0的线程,由此可得unparkSuccessor是按照顺序来决定先唤醒谁的,AQS中的队列是有优先级关系的。
这里LockSupport.unpark的时候很有可能后面的node还没有进行park,LockSupport.unpark支持预先解锁所以这里不会出现问题。这里还需要再次强调一个细节,就是compareAndSetWaitStatus,这次操作是为了让后继节点多进行一次自旋,减少park的几率。这些底层细微的细节一般人写代码的时候实在是难以想到
-
最后我们再稍微看一下ReentrantLock的公平锁实现
可以看出,公平锁对比非公平锁,一上来不会直接去尝试修改state,除非自己是第一个等待线程,AQS可以以线程的入队顺序来决定是否去抢占State的修改权