java线程 7大状态_Java多线程进阶(七)—— J.U.C之locks框架:AQS独占功能剖析(2)...

4e048b67527f64d95691de22be21fce5.png

一、本章概述

本章以ReentrantLock的调用为例,说明AbstractQueuedSynchronizer提供的独占功能。

本章结构如下:

以ReentrantLock的公平策略为例,分析AbstractQueuedSynchronizer的独占功能

以ReentrantLock的非公平策略为例,分析AbstractQueuedSynchronizer的独占功能

分析AbstractQueuedSynchronizer的锁中断、限时等待等功能

二、ReentrantLock的公平策略原理

本节对ReentrantLock公平策略的分析基于以下示例:

假设现在有3个线程:ThreadA、ThreadB、ThreadC,一个公平的独占锁,3个线程会依次尝试去获取锁:ReentrantLock lock=new ReentrantLock(true);

线程的操作时序如下:

//ThreadA lock

//ThreadB lock

//ThreadC lock

//ThreadA release

//ThreadB release

//ThreadC release

2.1 ThreadA首先获取到锁

ThreadA首先调用ReentrantLock的lock方法,我们看下该方法的内部:

d3d1e815288168246231f1acb7fad076.png

最终其实调用了FairSync的lock方法:

ce8a10eea4efaf507cd1d25f8fef9b83.png

acquire方法来自AQS:

611ba8750bbd744d406820c9bcfb720a.png

其中tryAcquire方法需要AQS的子类自己去实现,我们来看下ReentrantLock中的实现:

078a4e0cfe120da6bd42ab77576a151e.png

可以看到,在ReentrantLock中,同步状态State的含义如下:

State

资源的定义

0

表示锁可用

1

表示锁被占用

大于1

表示锁被占用,且值表示同一线程的重入次数

ThreadA是首个获取锁的线程,所以上述方法会返回true,第一阶段结束。(ThreadA一直保持占有锁的状态)

此时,AQS中的等待队列还是空:

31901e47b3963ccb7e5f98430460a913.png

2.2 ThreadB开始获取锁

终于,ThreadB要登场了,一样,ThreadB先去调用lock方法,最终调用AQS的acquire方法:

b3d116fc0104c447172aa7403c44f4a7.png

tryAcquire方法肯定是返回false(因为此时ThreadA占有着锁)。

接下来看下addWaiter方法,这个方法其实就是将当前调用线程包装成一个【独占结点】,添加到等待队列尾部。

e550f6464c0ee345684164baeea49b90.png

07d50b2e829122fd87a175cc5b7f18fb.png

这里关键是enq方法,因为并发插入的情况存在,所以该方法设计成了自旋操作,保证结点能成功插入,具体步骤如下:

①当队列为空的时候,先创建一个dummy头结点;

70adf0e34bfd31d23370c64ac6d2488e.png

②进入下一次循环,插入队尾结点。

936a28af004ce83b9c040ab77d0c7bf5.png

好了,ThreadB已经被包装成结点插入队尾了,接下来会调用acquireQueued方法,这也是AQS中最重要的方法之一:

87f8ac636cc91d96ee964422587e6b74.png

在AQS中,等待队列中的线程都是阻塞的,当某个线程被唤醒时,只有该线程是首结点(线程)时,才有权去尝试获取锁。

上述方法中,将ThreadB包装成结点插入队尾后,先判断ThreadB是否是首结点(注意不是头结点,头结点是个dummy结点),发现确实是首结点(node.predecessor==head),于是调用tryAcquire尝试获取锁,但是获取失败了(此时ThreadA占有着锁),就要判断是否需要阻塞当前线程。

判断是否需要阻塞线程:

b84438cef0b36a77db0398502ef82e5c.png

注意,对于独占功能,只使用了3种结点状态:

结点状态

描述

CANCELLED

1

取消。表示后驱结点被中断或超时,需要移出队列

SIGNAL

-1

发信号。表示后驱结点被阻塞了(当前结点在入队后、阻塞前,应确保将其prev结点类型改为SIGNAL,以便prev结点取消或释放时将当前结点唤醒。)

CONDITION

-2

Condition专用。表示当前结点在Condition队列中,因为等待某个条件而被阻塞了

对于在等待队列中的线程,如果要阻塞它,需要确保将来有线程可以唤醒它,AQS中通过将前驱结点的状态置为SIGNAL:-1来表示将来会唤醒当前线程,当前线程可以安心的阻塞。

看下图或许比较好理解:

①插入完ThreadB后,队列的初始状态如下:

6babdace1b373dad941fbef703e74581.png

②虽然ThreadB是队首结点,但是它拿不到锁(被ThreadA占有着),所以ThreadB会阻塞,但在阻塞前需要设置下前驱的状态,以便将来可以唤醒我:

0e8d43422526467148753d5d434ecea8.png

至此,ThreadB的执行也暂告一段落了(安心得在等待队列中睡觉)。

注意:补充一点,如果ThreadB在阻塞过程中被中断,其实是不会抛出异常的,只会在acquireQueued方法返回时,告诉调用者在阻塞器件有没被中断过,具体如果处理,要不要抛出异常,取决于调用者,这其实是一种延时中断机制。

98f819cd33d2c165417834c3fd9c22f9.png

2.3 ThreadC开始获取锁

终于轮到ThreadC出场了,ThreadC的调用过程和ThreadB完全一样,同样拿不到锁,然后加入到等待队列队尾:

a643e4cced6cb3f94bd49dd90afc9c8d.png

然后,ThreadC在阻塞前需要把前驱结点的状态置为SIGNAL:-1,以确保将来可以被唤醒:

e98ef7244047608686d8c5396150d415.png

至此,ThreadC的执行也暂告一段落了(安心得在等待队列中睡觉)。

2.4 ThreadA释放锁

ThreadA终于使用完了临界资源,要释放锁了,来看下ReentrantLock的unlock方法:

082cb34224731d7bfa92a820a7902c5a.png

unlock内部调用了AQS的release方法,传参1:

c45b5d9c7181bc0f0be22a2576fda112.png

尝试释放锁的操作tryRelease:

ce3e5a931aa56ce5263137e3916c5be1.png

释放成功后,调用unparkSuccessor方法,唤醒队列中的首结点:

9bf66dc6d53d3cceb964a7e344792ed2.png

此时,队列状态为:

dc96184111db7050b183124aa3110f25.png

2.5 ThreadB唤醒后继续执行

好了,队首结点(ThreadB)被唤醒了。

ThreadB会继续从以下位置开始执行,先返回一个中断标识,用于表示ThreadB在阻塞期间有没被中断过:

4921359eecf4ce7595df4171ccb48f90.png

然后ThreadB又开始了自旋操作,被唤醒的是队首结点,所以可以尝试tryAcquire获取锁,此时获取成功(ThreadA已经释放了锁)。

获取成功后会调用setHead方法,将头结点置为当前结点,并清除线程信息:

87f8ac636cc91d96ee964422587e6b74.png

39a7b6f7c897b25c920a44ccb0e99306.png

最终的队列状态如下:

b0d25c9660bc33c78f2c449825f9bd6f.png

2.6 ThreadB释放锁

ThreadB也终于使用完了临界资源,要释放锁了,过程和ThreadA释放时一样,释放成功后,会调用unparkSuccessor方法,唤醒队列中的首结点:

303fe23565e8cb02bc21446d7f6997dd.png

队首结点(ThreadC)被唤醒后,继续从原来的阻塞处向下执行,并尝试获取锁,获取成功,最终队列状态如下:

faa990f3d091dd91f9fd57bfcc01899b.png

2.7 ThreadC释放锁

ThreadC也终于使用完了临界资源,要释放锁了。释放成功后,调用unparkSuccessor方法,唤醒队列中的首结点:

此时队列中只剩下一个头结点(dummy),所以这个方法其实什么都不做。最终队列的状态就是只有一个dummy头结点。

9bf66dc6d53d3cceb964a7e344792ed2.png

至此,AQS的独占功能已经差不多分析完了,剩下还有几个内容没分析:

锁中断功能

限时等待功能

Conditon等待功能

这些功能将在后续章节陆续分析。

三、ReentrantLock的非公平策略原理

ReenrantLock非公平策略的内部实现和公平策略没啥太大区别:

非公平策略和公平策略的最主要区别在于:

公平锁获取锁时,会判断等待队列中是否有线程排在当前线程前面。只有没有情况下,才去获取锁,这是公平的含义。

9c9136a160a9b4c31b0f649a070d452b.png

非公平锁获取锁时,会立即尝试修改同步状态,失败后再调用AQS的acquire方法。

5896d2c150c76ee8ce36b42b1d1c9aeb.png

acquire方法会转调非公平锁自身的tryAcquire方法,其实最终是调了nofairTryAcquire方法,而该方法相对于公平锁,只是少了“队列中是否有其它线程排在当前线程前”这一判断:

5c059ebd0f9bb62ca16c19c467b9d537.png

四、AQS对中断的支持

还是以ReentrantLock为例,来看下AQS是如何实现锁中断和超时的。

我们知道ReentrantLock的lockInterruptibly方法是会响应中断的。(线程如果在阻塞过程中被中断,会抛出InterruptedException异常)

该方法调用了AQS的acquireInterruptibly方法:

92416bfa74fef13dfd5060ae98333ed1.png

上述代码会先去尝试获取锁,如果失败,则调用doAcquireInterruptibly方法,如下:

adfbaca97219df9dae410e99bb96a7e5.png

很眼熟有木有?看下和acquireQueued方法的对比,唯一的区别就是:

当调用线程获取锁失败,进入阻塞后,如果中途被中断,acquireQueued只是用一个标识记录线程被中断过,而doAcquireInterruptibly则是直接抛出异常。

5d820d3f18a6fcac13df8a346d08eeb3.png

五、AQS对限时等待的支持

Lock接口中有一个方法:tryLock,用于在指定的时间内尝试获取锁,获取不到就返回。

ReentrantLock实现了该方法,可以看到,该方法内部调用了AQS的tryAcquireNanos方法:

698bb0c4f6f9e4d60b9890b93a9d3487.png

tryAcquireNanos方法是响应中断的,先尝试获取一次锁,失败则调用doAcquireNanos方法进行超时等待:

880a4d92d450852f7e30187a02051375.png

关键是doAcquireNano方法,和acquireQuqued方法类似,又是一个自旋操作,在超时前不断尝试获取锁,获取不到则阻塞(加上了等待时间的判断)。该方法内部,调用了LockSupport.parkNanos来超时阻塞线程:

8bfcb8af33d56606cf2a9dd172ca6de8.png

LockSupport.parkNanos内部其实通过Unsafe这个类来操作线程的阻塞,底层是一个native方法:

5dadd658f25ce5a3466e2a723139b7e8.png

如果当前线程在指定时间内获取不到锁,除了返回false外,最终还会执行cancelAcquire方法:

1b5dd44f63d6cd81384e71404d82e14d.png

示例

为了便于理解还是以3个线程为例:

假设现在有3个线程:ThreadA、ThreadB、ThreadC,一个公平的独占锁,3个线程会依次尝试去获取锁,不过此时加上了限时等待:ThreadB等待10s,ThreadA等待20s。

ReentrantLock lock=new ReentrantLock(true);

//ThreadA tryLock

//ThreadB tryLock, 10s

//ThreadC tryLock, 20s

//ThreadA release

//ThreadB release

//ThreadC release

1. ThreadA首先获取到锁,ThreadB和ThreadC依次尝试去获取锁

ThreadB和ThreadC经过两轮自旋操作后,等待队列的情况如下:

e28cb4a6a0f0bf6ac2dfb2b7587f5a2c.png

2. ThreadB先到超时时间

调用了cancelAcquire方法取消操作,队列状态变成:

59e603105f9585f4f2b2f5fe6b074235.png

3. ThreadC到达超时时间

调用了cancelAcquire方法取消操作,队列状态变成:

19abf300ce06d4e4d2d621c41de3ea8f.png

在退出cancelAcquire后,原来ThreadB和ThreadC对应的结点会被JVM垃圾回收器回收。

六、总结

本章从ReentrantLock入手,分析AQS的独占功能的内部实现细节。下一章,从CountDownLatch入手,看下AQS的共享功能如何实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值