本文从源码出发,快速过了一遍ReentrantLock独占锁的实现,觉得有点乱的,请谅解。
ReentrantLock内部类Sync继承自AQS,实现了AQS的自定义同步器。
公平锁和非公平锁分别由继承于Sync的FairSync和NonfairSync实现,默认构造函数实现的为非公平锁,构造函数中传参为true为公平锁实现。
非公平锁:
可以看到加锁实现为
1=>compareAndSetState(0,1)通过cas修改同步状态,修改state为1即代表加锁成功,设置当前AQS独占线程为当前线程。
2=>加锁失败调用acquire(1)。
接下来看acquire(1)方法
可以看到acquire(1)方法为AQS的实现,继续往下看
1.tryAcquire(1)方法
AQS仅定义了该方法,具体方法在ReentrantLock中NonfairSync实现。
直接看nonfairTryAcquire(1)方法
(1)直接获取当前同步状态c,c为0,则当前锁还未被其他线程获取,直接抢锁,成功就设置当前线程为独占线程,结束。
(2)c不为0,锁为可重入锁,判断锁独占线程为当前线程,同步状态+1,设置同步状态,结束(这里就可以看出来AQS默认是不支持重入的,ReentrantLock实现的tryAcquire方法支持了)。
2.tryAcquire(1)获取锁失败,就会走acquireQueued(addWaiter(Node.EXCLUSIVE), 1)方法
(1)Node.EXCLUSIVE,可以看AQS内部类Node节点。
设置节点默认为独占节点,从注释可以看出标识该节点在独占模式下等待,该节点的waitStatus默认为0,waitStatus的变更后续会提及。
(2)addWaiter(node)方法
从这里可以看出,抢锁失败的线程,其实是将线程封装为一个Node添加到了一个队列中等待了,该队列是CLH队列的变体虚拟双向队列(FIFO)。
1.先按顺序看代码,尾节点不为空时,用pred引用指向尾节点,将当前节点prev前驱节点指向pred,将尾节点的值设置为node,pred的后继节点设置为node,这样就完成了node和尾节点的替换,node成为最新的尾节点。
2.对于添加的第一个节点,尾节点是为空的,会走enq(node)方法,进行头尾节点的初始化和node的添加,源码见下图
见源码可知,会一直循环尝试添加节点,如果尾节点为空,则新建一个虚拟头节点,将尾节点指向头节点;
尾节点不为空,则将当前节点设置为尾节点。
addWriter()方法可知,当队列为空时,会初始化一个虚拟节点作为头节点,将当前线程封装为node节点,向队列中添加node节点,其中都是通过cas设置节点值;队列已经有元素时就将其添加到尾节点。
(3)acquireQueued(node,1)方法
经过上面得步骤,已经将一个获取锁失败的线程添加到等待队列中去了,acquireQueued方法会把放入队列中的线程不断去获取锁,直到获取成功或者判断该node线程是需要挂起的。
对于我们添加进队列的节点node,会循环判断node的前驱节点是否是头节点,若是头节点则可以一直循环去抢锁,此时锁已被其他线程获取随时有可能释放锁,所以对于头节点后的第一个节点,就需要一直去抢锁,抢到锁后将当前节点设置为头节点,通过setHead方法,将node节点不必要的值设置为了null(虚拟节点)。
为了避免添加的节点去一直无限循环占用资源,对于节点需要挂起线程,等待唤醒,在源码
1=>就是判断node的线程是否被中断
1.shouldParkAfterFailedAcquire()方法看看源码
通过注释可看出该方法就是检查和更新抢锁失败节点的状态,如果前驱节点待唤醒就返回true。
通过源码可以看出,对于我们新添加的这个node,前驱节点pred的waitStatus是为0,所以直接走compareAndSetWaitStatus(pred, ws, Node.SIGNAL)这个方法,通过cas将pred的waitStatus设置为Node.SIGNAL即-1,在第一次循环抢锁还没抢到后就设置pred的waitStatus为-1了,在第二次循环抢锁失败就是走第一个ws == Node.SIGNAL判断,代表pred是待唤醒的节点,那么就将node节点线程中断挂起。
若ws > 0则ws只有一个状态为Node.CANCELLED即1,为取消状态,对于取消状态的节点需要跳过,保证了node前面节点不会存在被取消的无效节点。
2.node判断为可以挂起则走parkAndCheckInterrupt()方法,挂起当前线程,阻塞调用栈,返回当前线程的中断状态。
cancelAcquire(node)方法
若加锁期间发生了异常则取消该节点,将节点状态设置为Node.CANCELLED,看看源码,代码较长,简短总结。
1.节点为空,直接返回。
2.跳过node前面取消状态的节点,设置node的waitStatus状态为取消状态即1
3.如果该节点为尾节点,就简单了直接设置node的前驱节点为尾节点,设置成功后将尾节点的后继节点为null,就将node节点移除掉了
4.若node节点的前驱节点为头节点,则通过unpartSuccessor()方法将该节点的后继节点唤醒(如果是node是非取消节点正常唤醒后节点,则会将node的waitStatus设置为0)。
5.若node节点的前驱节点pred不为头节点,pred的waitStatus为Node.SIGNAL或者pred的waitStatus可设置为Node.SIGNAL,且pred的线程不为空则将pred的后继指针指向node的后继节点,如下图所示:
从上述一系列操作可以看出,虽然将CANCELLED状态的节点跳过了,其实节点仍然是存在于队列中的,只是pred的后继指针指向了node的后继节点了,node的前驱指针仍然指向pred节点,node后继节点的前驱指针仍然指向node。通过上面的流程,对于CANCELLED节点状态的所有的变化都是对Next指针进行了操作,而没有对prev指针进行操作?为啥?
在执行cancelAcquire的时候,当前节点的前置节点可能已经从队列中出去了(已经执行过shouldParkAfterFailedAcquire方法了),如果此时修改prev指针,有可能会导致prev指向另一个已经移除队列的node,因此这块变化prev指针不安全。 shouldParkAfterFailedAcquire方法中,会执行下面的代码,其实就是在处理prev指针。shouldParkAfterFailedAcquire是获取锁失败的情况下才会执行,进入该方法后,说明共享资源已被获取,当前节点之前的节点都不会出现变化,因此这个时候变更prev指针比较安全。
以上加锁流程已走完,后面看解锁流程,解锁流程并不区分公平锁和非公平锁
1.解锁源码
tryRelease方法判断当前线程为独占线程,抛出异常,基本不会有这个问题。如果c=0,设置独占线程为null,接下来设置state的值,结束。
再回到release方法继续往下走,锁释放成功了,如果头节点不为空,当h.waitStatus < 0表示头节点后有阻塞且有效的后继节点,h.waitStatus == 0表示头节点后有正在运行的后继节点,所以判断h.waitStatus != 0 时才去唤醒后继节点,则unparkSuccessor方法唤醒head的后继节点,该方法在shouldParkAfterFailAcquire里的cancelAcquire中也有同样的方法,看看源码
看代码可知,如果ws<0,就cas更新节点的waitStatus为0,下面就对可能的取消状态做了处理,会一直从后往前循环找到waitStatus <= 0的节点s,将s唤醒。
为啥要从后往前找呢?之前已经有所提及了,在addWaiter时,节点入队并不是原子操作,可能会存在节点入队,但是next指针并没有赋值的情况,所以只有prev指针是完整的,node.prev = pred; compareAndSetTail(pred, node) 这两行代码可以看作Tail入队的原子操作,在生成取消节点时也是,都是断开的next指针,有可能导致无法遍历全所有的节点。
s节点唤醒后,从这个地方继续执行,继续去抢锁,结束。
当中断线程被唤醒时,并不知道被唤醒的原因,可能是当前线程在等待中被中断,也可能是释放了锁以后被唤醒。因此通过Thread.interrupted()方法检查中断标记(该方法返回了当前线程的中断状态,并将当前线程的中断标识设置为False),并记录下来,如果发现该线程被中断过,就再中断一次。
线程在等待资源的过程中被唤醒,唤醒后还是会不断地去尝试获取锁,直到抢到锁为止。也就是说,在整个流程中,并不响应中断,只是记录中断记录。最后抢到锁返回了,那么如果被中断过的话,就需要补充一次中断。
公平锁有点区别
在调用lock方法时,直接调用acquire(1)方法,就没有先抢锁的步骤了,一个比较大的区别是tryAcquire方法,见源码
hasQueuedPredecessors()方法是公平锁加锁时判断等待队列中是否存在有效节点的方法。如果返回False,说明当前线程可以争取共享资源;如果返回True,说明队列中存在有效节点,当前线程必须加入到等待队列中。
顺便提一嘴读写锁的几个要点
ReentrantReadWriteLock里的写锁、读锁在加锁时也是有公平锁和非公平锁的,公平、非公平的实现见红框这行。
如果是非公平锁直接返回false,然后去抢锁。公平锁就还是走的hasQueuedPredecessors()方法,判断是否已经有了有效节点。
exclusiveCount()就是得到写锁的获取的次数,通过同步状态state的低16位来算写锁获取次数w,高16位来算读锁获取次数,具体实现点进去exclusiveCount()方法就知道了。写锁的释放直接看源码很清晰。
读锁源码的加锁方法tryAcquireShared()和释放锁方法tryReleaseShared()有点长,有精力再写。
读写锁支持锁降级,遵循按照获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁,不支持锁的升级。