ReentrantLock 加锁和解锁过程
2022-06-26 22:22:16 0 举报
ReentrantLock 加锁和解锁的主线: 加锁:1. 尝试获取一次锁;2. 获取锁失败,将线程封装成Node,并加入到队列中;3. 阻塞排队的线程,并自旋尝试获取锁。 解锁:1. 尝试释放锁;2. 不持有锁以后,唤醒阻塞中的后续线程。
作者其他创作
大纲/内容
公平锁实现的尝试获取逻辑
acquireQueued()
s.thread != Thread.currentThread()
p == head?
阻塞队列
1. 尝试获取一次锁
tryRelease()
是
当前 node 中的线程就在此等候阻塞着,只有当持有锁的线程通过 unlock 释放锁以后,它才可以结束阻塞,再进行抢锁
false,pred.waitStatus == 0(无具体含义)
AQS队列(双向链表)
FairSync#lock()
2. 加锁失败,线程加入队列
尝试获取锁,如果获取不到,排队等待(阻塞当前线程)
返回 node
false
返回 true,表示需要排队
true
继任者是否存在并且没有被取消
注意,当前持有锁的线程,永远不会处于阻塞队列中。这是因为当一个线程调用 lock 获取锁时,只有两种情况:1) 锁处于自由状态(state == 0),此时它不需要排队。通过 CAS 立刻获取到了锁,这种情况,显然它不会处于阻塞队列中;2) 锁处于非自由状态,线程加入到了阻塞队列的队头(不包括 head)等待锁。将来某一时刻,锁被其他线程释放,并唤醒这个排队的线程,线程被唤醒后,执行tryAcquire,获取到了锁,然后重新维护阻塞队列,将自己从队列移除(acquireQueued 方法)。所以,不管哪种情况,持有锁的线程,永远不会处于阻塞队列中。
调用 setExclusiveOwnerThread,设置当前线程为所持有线程
重入锁计时器加1
整个 tryAcquire 返回true,表示获取锁成功,仍然是当前线程持有锁
没有其他线程在排队
① 线程尝试释放持有的锁
获取 node 的前驱节点final Node p = node.predecessor()
sync.lock()
s 是第二个节点(第一个节点为空节点),代表下一个应该获取锁的线程。s.thread != Thread.currentThread() 为 false,表示二者相等,即下一个可以获得锁的线程,正是当前线程,所以,此时整个 hasQueuedPredecessors 返回 false,表示不需要排队
阻塞结束
prev
node.waitStatus < 0 ?
head
表示获取到锁:1. node 设置为头结点2. 释放之前的头结点这个逻辑执行完成后, node 节点就不再排队了(获取到锁的线程,不会在阻塞队列中了)
tryRelease 的执行流程
== 0,没有线程占用锁
返回false,线程没有获取到锁
根据 park 方法 API 描述,程序在下述三种情况会继续向下执行1. 被 unpark2. 被中断(interrupt)3. 其他不合逻辑的返回因上述三种情况程序执行至此,返回当前线程的中断状态,并清空中断状态如果当前线程是被中断的,该方法会返回 true
整个AQS的核心和难点之一,只有这里使用了for(;;)首先判断 node 的前驱结点是不是 head,如果是,说明它是下一个可以获得锁的线程,则调用一次tryAcquire,尝试获取锁。若获取到锁,则重新维护链表(将 node 设置为 head,之前的 head 从链表移除),然后返回。如果 node 的前驱节点不是 head,或获取锁失败,则判断其前驱节点的 waitStatus,是不是Node.SIGNAL,如果是,则当前线程调用 LockSupport.park(this),进入阻塞状态;如果不是:1. ==0,则设置为 Node.SIGNAL;2. > 0(即 == 1),则表示前驱节点已经获取锁了,将获取锁的节点,从队列移除,重新维护队列链表关系。然后,再次进入 for 循环,上面的逻辑重新执行一遍。注意,它和 doAcquireInterruptibly 方法相比,两者的主要区别是发现线程被中断过之后的处理逻辑。
当前线程不是持有锁的线程,则抛异常
将当前线程封装成 node 对象,并加入到阻塞队列中。根据阻塞队列是否执行过初始化,执行 ①、② 两段不同的逻辑。①:表示阻塞队列不为空,也就是说之前已经初始化过了,此时只需要间新的 node 加入到阻塞队列末尾即可。②:表示阻塞队列为空,需执行队列的初始化。enq() 会初始化一个空的 node,作为阻塞队列的 head,然后将需要排队的线程作为 head 的 next 节点插入。
pred.waitStatus 等于 SIGNAL(即 == -1)
队列不是空的
无论返回 true 还是 false,都会再次进入 for 循环,接着抢锁。若返回 true,则 acquireQueued() 结束后会中断当前线程
判断当前线程是否与持有锁的线程相等这就是重入锁的实现原理
unparkSuccessor 的执行流程
tryAcquire
更新 state(锁标记)
执行 LockSupport.unpark(s.thread) 释放线程
队列已经初始化
abstract void lock();
直接返回 false,继续自旋抢锁
getState() - releases == 0 ?
tail
成功获取到锁,设置锁持有线程为当前线程,返回。
Node: n1thread = nullprev = nullnext = n2
则释放锁完成,后面没有等待获取锁的线程了
pred.waitStatus == Node.SIGNAL?
next
NonfairSync#lock()
pred.waitStatus > 0?
将哨兵节点的 waitStatus 的值置位 0
返回 true,然后得到阻塞队列的头节点 head
2. 唤醒后续节点
acquire(1)
解锁主线总结:ReentrantLock解锁的过程,可以分为两个方面:1. 尝试释放锁,state 减一、持有锁的线程标记为 null2. 唤醒后续线程,然后让继任者继续抢锁依据上面三条主线,逐步分析 ReentrantLock 的底层源码
表示线程获取到锁,线程逐级返回,加锁过程结束
head.waitStatus != 0
ReentrantLock.Sync#tryRelease
false,没有初始化
是不是应该阻塞后续获取锁失败的节点该方法接收两个 node 对象参数参数 1 是前驱节点 prev参数 2 是准备执行 park 操作的节点 node
取消排队
true,已经初始化
acquireQueued 的执行流程
说明节点 node 多次抢锁都失败了,需要等待被占用的资源释放。直接返回 true,需要继续调用 parkAndCheckInterrupt()
将 node 加入到队列末尾
addWaiter 的执行流程
表示当前线程持有的是一个重入锁,锁还被该线程继续持有,这只是释放了一次锁。最终函数返回 false
整个 tryAcquire 返回false,表示获取锁失败
从尾部向前移动,查找实际未取消且有效的继任者
队列没有初始化,则此时 h == t== null,所以 h != t 返回 false。那么整个 hasQueuedPredecessors 方法也会返回 false,表示不需要排队
公平锁
否
设置当前持有锁的线程为 null,最终函数放回 true
Node: n3thread = T3prev = n2next = null
执行 unparkSuccessor(h) 来唤醒后续节点
从这里开始ReentrantLock
队列元素等于1
true,pred.waitStatus == CANCELLED(即 == 1)
selfInterrupt()中断当前线程
队列尚未初始化,调用这个 enq() 方法该方法生成一个空的 node 对象(new Node()),插入到 AQS 队列头部,然后将参数 node,作为其后继几点,插入队列。
sync.release(1)
在节点 n3 插入之前,已经有一个节点 n2,在排队了
sync 继承自 AbstractQueuedSynchronizer(简称AQS)入参 1 表示所释放后 state(锁标记)减一
执行步骤 ②
线程挂起,程序不会继续向下执行
非公平锁实现的尝试获取逻辑
注意:入参 node 为队列的哨兵节点
sync 继承自 AbstractQueuedSynchronizer(简称AQS)
队列是否已初始化(h != t ?)
①
② 解锁完成后,唤醒后续节点,让后继者继续抢锁
判断在当前线程前,是否存在其他线程在排队,如果有,那么当前线程也需要排队
true,node 的前驱节点是哨兵节点说明 node 排在队列头部,则尝试获取一次锁
执行 parkAndCheckInterrupt() 中的 LockSupport.park(this) 后,线程被阻塞
在尝试获取锁的时候,有以下几种情况,会导致获取锁失败:1. 锁被其他线程获取了;2. 当前线程需要排队获取锁;3. 通过CAS操作标记state值的时候失败,说明已经被其他线程拿到锁了
返回 false,表示不需要排队
元素个数 > 1?
!= 0,有线程正在占用锁
null
一、先加锁
addWaiter(Node.EXCLUSIVE)
二、再解锁
unlock()
队列是否为空
无效或者被取消
队列元素大于1
二者不相等,说明下一个可以获得锁的线程,不是当前线程,所以整个 hasQueuedPredecessors 返回 true,表示需要排队
node.next 得到哨兵节点的下一个节点(称之为继任者)
① 这部分都是由 tryAcquire() 方法调用引申出来的逻辑
parkAndCheckInterrupt()线程进入阻塞状态
③ node 加入队列后的处理逻辑,首先会进行一次自旋,尝试获取锁,获取不到,才调用 LockSupport.park(),阻塞自己
②
继任者有效
tryAcquire(arg)
返回 true
执行步骤 ①
hasQueuedPredecessors 的执行流程
lock()
调用 enq() 构造一个队列,并将 node 加入到队列
加锁主线总结:ReentrantLock加锁的过程,可以分为三个阶段 : 1. 尝试加锁一次 2. 加锁失败,线程加入队列 3. 阻塞排队线程,自旋尝试获取锁依据上面三条主线,逐步分析 ReentrantLock 的底层源码
Node: n2thread = T2prev = n1next = n3
返回true,表示获取到了锁,线程逐级返回,加锁过程结束
队列中的 head 节点是空节点,它是系统创建的哨兵节点,占位用
释放锁结束
shouldParkAfterFailedAcquire 的执行流程
AQS有两个属性 head 和 tail,分别用来保存 AQS 队列的头尾节点。初始化时,这两个属性都是 null;当有一个线程排队时,队列如下:
整个 tryAcquire 返回true,表示获取锁成功
pred != null
h != t 返回 false
Node: n2thread = T2prev = n1next = null
非公平锁
getState() == 0
3 阻塞排队线程,自旋尝试获取锁
tryAcquire 的执行流程
parkAndCheckInterrupt() 中的Thread.interrupted()
当最后一个排队线程也获取到锁以后,此时 head == tail,且 != null。
返回false,表示锁还没有释放完,只是释放了一个锁标记,重入锁需要多次释放。
CAS获取锁
hasQueuedPredecessors()
此时至少有2个元素,h.next 指向第二个元素,肯定不是 null,所以 (s=h.next) == null 返回 false
强制子类实现
找到了继任者
通过 CAS 操作,将前驱节点的 waitStatus 的置为 SIGNAL(即 -1),用于后续挂起操作
head == null ,队列为空
② 尝试获取锁失败时,将当前尝试获取锁的线程封装成 Node 对象,并加入到 AQS 队列中
注意,这里是设置 pred 节点,而不是 node 节点的 waitStatus。为何每个线程进入该方法后,修改的是上一个节点的 waitStatus,而不是修改自己的你?因为线程调用 park 后,无法设置自己的这个状态,若在调用 park 前设置,存在不一致的问题。所以,每个 node 的 waitStatus,在后继结点加入时设置。由此可以看出线程被挂起,需要自旋两次,第一次是给前驱节点设置 SIGNAL 状态,第二次判断前驱节点状态为 SIGNAL 然后进行挂起操作
waitStatus 值说明:1. SIGNAL:值为 -1,此节点的后继节点是(或即将是)被阻塞状态(通过 park)2. CANCELLED:值为 1,由于超时或中断,此节点被取消
释放线程,结束阻塞状态
1. 尝试释放锁
current == getExclusiveOwnerThread()
整个 hasQueuedPredecessors 方法返回 false,表示不需要排队
表示 node 的前驱节点,已经取消了,此时需要重新维护链表关系,即循环判断前驱节点的前驱节点是否也为 CANCELLED 状态,忽略所有该状态的节点。
这个队列表示,在节点 n2 插入队列之前,没有其他节点在排队。
收藏
0 条评论
下一页