ReentrantLock 之 lock 加锁过程分析
2020-02-06 16:04:51 27 举报
ReentrantLock 是 Java 并发包中的一个类,它实现了 Lock 接口。在 ReentrantLock 中,lock() 方法用于加锁。当一个线程调用 lock() 方法时,如果锁当前没有被其他线程持有,那么该线程将成功获取锁并返回 true;如果锁已经被其他线程持有,那么该线程将被阻塞,直到锁被释放。 ReentrantLock 的加锁过程可以分为以下几个步骤: 1. 首先检查锁是否已经被占用,如果已经被占用则进入等待队列。 2. 如果锁未被占用,则将锁分配给当前线程,并将锁标志位设置为已占用。 3. 如果当前线程已经拥有了锁,则直接返回 true。
作者其他创作
大纲/内容
Node: n2thread = t2prev = n1next = null
false
表示获取到锁:1、node 设置为头结点2、释放之前的头结点这个逻辑执行完成后,node 节点就不再排队了(获取到锁的线程,不会在排队队列中了)
tryAcquire 方法,尝试获取锁。以下几种情况,会导致获取锁失败:1、锁已经被其他线程获取;2、锁没有被其他线程获取,但当前线程需要排队;3、cas 失败(可能过程中已经有其他线程拿到锁了)锁为自由状态(c == 0),并不能说明可以立刻执行 cas 获取锁,因为可能在当前线程获取锁之前,已经有其他线程在排队了,必须遵循先来后到原则获取锁。所以还要调用hasQueuedPredecessors方法,查看自己是否需要排队。
整个 tryAcquire 返回 false,表示获取锁失败
这部分所有逻辑,都是 tryAcquire 方法调用引申出来的。tryAcquire 方法,顾名思义,就是尝试获取一次锁。
== 0 表示锁为自由状态
p == head?表示查询 node 是不是第一个排队的
整个 hasQueuedPredecessors 返回 false,表示不需要排队。
hasQueuedPredecessors 方法执行流程
这部分,是 node 加入队列后的处理逻辑,这里会有一次自旋,尝试获取锁,获取不到,才调用 park,阻塞自己。
队列元素等于 1
pred.waitState > 0?
true
返回 true,表示获取到了锁,线程逐级返回,加锁过程结束。
tail
返回 node
hasQueuedPredecessors()
Node: n2thread = t2prev = n1next = n3
s.thread != Thread.currentThread()
sync.lock()
是
next
h != t 返回false
否
getState() == 0?
返回 false
Node: n1thread = nullprev = nullnext = null
尝试获取锁,如果获取不到,排队(阻塞当前线程)
再次循环判断
abstract void lock()
AQS 有两个属性 head 和 tail,分别用来保存 aqs 队列的头尾结点。初始时,这两个属性都是 null。当有一个线程排队,队列如下
返回 true,表示需要排队
尝试获取锁
shouldParkAfterFailedAcquire 方法详解
设置重入锁计数器
返回 false,表示无需排队
返回 false,表示线程没有获取到锁。
acquireQueued 方法详解
enq 方法
Node: n3thread = t3prev = n2next = null
true,执行上图步骤 1
lock
tryAcquire
注意,这里是设置 pred 节点,而不是 node 节点的 waitState。-1 表示节点处于阻塞状态了。为何每个线程进入该方法后,修改的是上一个节点的 waitState,而不是自己修改自己的?因为线程调用 park 后,无法设置自己的这个状态,若在调用 park 前设置,存在不一致的问题。所以,每个 node 的 waitState,在后继加点加入时设置。
没有
将当前线程封装成 Node对象,并加入排队队列中。根据排队队列是否执行过初始化,执行 1、2 不同处理逻辑。1:表示排队队列不为空,即之前已经初始化过了,此时只需将 新的 node 加入排队队列末尾即可。2:表示排队队列为空,需执行队列初始化。enq 会初始化一个 空的 Node,作为排队队列 的head,然后将需要排队的线程,作为 head 的 next 节点插入。
shouldParkAfterFailedAcquire(pred,node)
返回 true
acquire(1)
head
整个 aqs 的核心和难点之一。注意这里使用了 for(;;)首先判断 node 的前辈节点,是不是 head,如果是,说明它是下一个可以获得锁的线程,则调用 一次 tryAcquire,尝试获取锁,若获取到,则将链表关系重新维护下(node设置为 head,之前的 head从链表移出),然后返回。如果 node 的前辈节点不是 head,或获取锁失败,再判断其前辈节点的 waitState,是不是 SIGNAL,如果是,则当前线程调用 park,进入阻塞状态。如不是:1、== 0,则设置为 SIGNAL;2、>0 (==1),则表示前辈节点已经被取消了,将取消的节点,从队列移出,重新维护下排队链表关系。然后再次进入 for 循环,上面的逻辑重新执行一遍。注意和 doAcquireInterruptibly 方法对比,二者区别主要在,发现线程被中断过之后的处理逻辑。
元素个数 > 1
设置 pred.waitState = -1
FairSync#lock()
prev
执行排队逻辑
这个队列表示,在节点 n2 插入队列之前,没有其他节点在排队。注意,当前持有锁的线程,永远不会处于排队队列中。这是因为:当一个线程调用 lock 获取锁时,只有两种情况:1)锁处于自由状态(state==0),且它不需要排队,通过 cas 立刻获取到了锁,这种情况,显然它不会处于 排队队列中;2)锁处于非自由状态,线程加入到了排队队列的队头(不包括 head)等待锁。将来某一时刻,锁被其他线程释放,并唤醒这个排队的线程,线程唤醒后,执行 tryAcquire,获取到了锁,然后重新维护排队队列,将自己从队列移出(acquireQueued 方法)。所以,不管哪种情况,持有锁的线程,永远不会处于排队列表中。
tryAcquire 方法执行流程详解
Node: n1thread = nullprev = nullnext = n2
公平锁
aqs 队列(链表)
整个 tryAcquire 返回 true,表示获取锁成功
sync 继承了 AbstractQueuedSynchronizer。
已经初始化
pred != null
CAS获取锁
parkAndCheckInterrupt线程进入阻塞状态
false(即 == 0)
成功获取到锁,设置锁持有线程为当前线程。返回。
判断之前是不是当前线程获取到锁的
队列是否初始化?
执行 park 逻辑
这部分逻辑,是尝试获取锁失败的情况下,当前线程(尝试获取锁的)封装成 Node 对象,加入到 aqs 队列中的处理逻辑。
addWaiter
这个方法,接收两个 Node 对象参数:参数 2 是准备执行 park 操作的节点 node ,参数 1 是其前辈节点 pred。
队列尚未初始化,调用这个 enq 方法。该方法生成一个空的 Node 对象(new Node(),右图粉色部分),插入到 aqs 队列头部,然后将参数 node,作为其后继节点,插入队列,方法执行完毕,队列如右图。
非公平锁
pred.waitState == Node.SIGNAL(-1)?
当最后一个排队线程也获取到锁之后,此时 head = tail,且 != null。排队队列如左图。
重入锁的实现原理
队列没有初始化,则 h != t 返回 false,因为此时 h == t == null。整个 hasQueuedPredecessors 方法返回 false,表示不需要排队。
false,执行上图步骤 2
网上讲述 ReentrantLock 加锁过程的文章很多,但不一定讲清楚了。这幅图如果你能看懂,保证你能明白 ReentrantLock 加锁过程!基于 jdk 1.7/1.8 。整个 ReentrantLock 的加锁过程,可以分为三个阶段:1、尝试加锁;2、加锁失败,线程入队列;3、线程入队列后,进入阻塞状态。对应下面 ① ② ③ 三部分。
队列元素大于 1,则至少为 2 个,h.next 指向第二个元素,肯定不是 null,所以(s=h.next) == null 返回 false
acquireQueued
调用setExclusiveOwnerThread,设置当前线程为锁持有线程
队列已经初始化,将 node 加入到队列末尾,加入后队列可能如下
整个 tryAcquire返回 false,表示获取锁失败
ReentrantLock
二者不相等,说明下一个可以获得锁的线程,不是当前线程,所以整个 hasQueuedPredecessors 返回 true,表示需要排队。
false 再次循环判断
aqs 队列(链表)
acquireQueued 返回,表示线程获取到锁,线程逐级返回,加锁过程结束。
这个队列表示,在 节点 n3 插入之前,已经有一个节点 n2,在排队获取锁了。
s 是第二个节点(第一个节点是空节点),代表下一个应该获取锁的线程。s.thread != Thread.currentThread() 为 false,表示二者相等,即下一个可以获得锁的线程,正是当前线程,所以,此时整个 hasQueuedPredecessors 返回 false,表示不需要排队。
!=0 表示不是自由状态
阻塞结束
NonFairSync#lock()
addWaiter 详解
没有其他线程在排队,调用 enq 构造队列,并将 node 加入到队列。
两种情况,会导致阻塞结束:1、持有锁的线程,释放锁后,将这个线程 unpark 了。此时该线程,一定排在队列的队头(不包括 head 节点);2、线程被 interrupt 了。(注意,在外部 interrupt 这个线程,不是抛出 InterruptException,这一点和 sleep、wait 阻塞不一样)
判断在当前线程前面,是否已经有其他线程在排队了,有,则表示当前线程也需要排队
队列元素大于 1
0 条评论
下一页