我没看太精通,只是自己当笔记,大家慎看
1. AQS
是干什么的
1.1 在历史长河中
最开始在jdk1.6以前只有synchronized
的重量级锁,调用的是底层的native
方法,也就是会去调用操作系统的函数,因为要操作操作系统的函数所以有用户态–>内核态的状态的切换,所以是重量级
这个时候Doung Lea
大哥看不惯了,开发了JUC
这个包,让解决同步不要再去操作系统层面去做,可想而知比synchronized
更快;对于JUC
并发包下的同步类(Lock
、Semaphore
、ReentrantLock
等)都是基于AbstractQueuedSynchronizer
(简称为AQS
)实现的。AQS
,即AbstractQueuedSynchronizer
, 队列同步器,它是Java并发用来构建锁和其他同步组件的基础框架
AQS
是一个抽象类,主是是以继承的方式使用。AQS
本身是没有实现任何同步接口的,它仅仅只是定义了同步状态的获取和释放的方法来供自定义的同步组件的使用。从图中可以看出,在java的同步组件中,AQS
的子类(Sync
等)一般是同步组件的静态内部类,即通过组合的方式使用
后来又升级到jdk1.7,sun公司也去升级了synchronized
,也就是升级了轻量级锁,偏向锁
1.2 从代码层面看看AQS
是干嘛的
就像上面说的AQS
是一个抽象类,主是是以继承的方式使用,在之前学习的锁,信号量等你会发现他们很相似都是为了让部分线程等待让线程协作,同步,既然有这样的共同点,就能为他们都提取出一个工具类,可以复用,对于ReentrantLock
和Semaphore
而言就可以屏蔽很多细节,只关注自己的业务逻辑就行了
对于Semaphore
就是这样的,在内部有一个Sync
类,Sync
类继承了AQS
,实现了部分方法
我们去看源码你会发现ReentrantLock
等都是使用了AQS的技术底层使用自旋+CAS+park
这三大技术栈实现的,而且在单个线程或线程交替执行的时候,是在jdk级别解决同步问题,且和队列无关
1.3 AQS的作用
- 同步状态的原子性管理
- 线程的阻塞与解除阻塞
- 队列的管理
在并发的场景下,我们正确并高效的实现这些内容是相当复杂的,所以我们使用AQS来帮助我们搞定,自己只需要关心业务逻辑
AQS是一个用于构建锁,同步器,协作工具类的工具类(框架),有了AQS后,更多的协作工具都可以很方便的被写出来
有了AQS,构建线程协作类就容易多了
2. AQS的基本使用
AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中
2.1 AQS的核心部分
① state状态
② 控制线程抢锁和配合的FIFO队列
③ 期望协作工具类去实现的获取/释放等重要方法
2.1.1 state状态
/**
* The synchronization state.
*/
private volatile int state;
state
的具体含义会根据具体实现类的不同而不同
Semaphore
中,他表示"剩余的许可证是数量"- 在
CountDownLatch
中,他表示"还需要倒数的数量" - 在
ReentrantLock
中,state用来表示锁的占有情况,包括可重入计数
state
方法都是volatile
修饰的, 会被并发地修改,所以所有的修改state
的方法都需要保证线程安全,比如getState
,setState
,以及compareAndSetState
操作来读取和更新这个状态,这些方法都依赖于j.u.c.atomic
包的支持
2.1.2 控制线程抢锁和配合的FIFO队列
这个队列是用来存放等待线程的,AQS就是排队管理器,当多个线程争用同一把锁时,必须有排队机制将那些没能拿到锁的线程串在一起,当锁释放的时候,锁管理器就会挑选一个合适的线程来占有这个刚刚释放的锁
AQS会维护一个等待的线程队列(双向链表形式),把线程都放到在这个队列里
* +------+ prev +-----+ +-----+
* head | | <---- | | <---- | | tail
* +------+ +-----+ +-----+
2.1.3 期望协作工具类去实现的获取/释放等重要方法
获取
获取操作会依赖state
变量,经常会阻塞(比如获取不到锁的时候)
在Semaphore
中,获取就是acquire
方法,作用是获取一个许可证
在CountDownLatch
中,获取是await
方法,作用是等待,直到倒数结束
释放方法
释放操作不会阻塞
在Semaphore中,释放就是release方法,作用是释放一个许可证
在CountDownLatch中,释放就是countDown方法,作用是倒数一个数
2.2 AQS的用法
AQS详解
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样
① 第一步:写一个类,想好协作逻辑,实现获取/释放方法
② 第二步:内部写一个Sync
类继承AbstractQueuedSynchronizer
,根据是否独占来重写tryAcquire()
/tryRelease()
或者tryAcquireShared(int acquires)
/tryReleaseShared(int release)
等方法,在之前写的获取/释放方法中调用AQS的acquire()
/release()
或者`shared``方法
③ 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法
自定义同步器在实现的时候只需要实现共享资源state的获取和释放方式即可,至于具体线程等待队列的维护,AQS已经在顶层实现好了。自定义同步器实现的时候主要实现下面几种方法:
isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
获取锁的姿势
//如果获取锁失败
if(!tryAcquire(arg)){
//入队,可以选择阻塞当前线程 park
}
释放锁的姿势
//如果释放锁成功
if(tryRelease(arg)){
//让阻塞的线程恢复运行
}
使用AQS实现互斥锁
class Mutex implements Lock, java.io.Serializable {
// 自定义同步器
private static class Sync extends AbstractQueuedSynchronizer {
// 判断是否锁定状态
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 尝试获取资源,立即返回。成功则返回true,否则false。
public boolean tryAcquire(int acquires) {
assert acquires == 1; // 这里限定只能为1个量
if (compareAndSetState(0, 1)) {//state为0才设置为1,不可重入!
setExclusiveOwnerThread(Thread.currentThread());//设置为当前线程独占资源
return true;
}
return false;
}
// 尝试释放资源,立即返回。成功则为true,否则false。
protected boolean tryRelease(int releases) {
assert releases == 1; // 限定为1个量
if (getState() == 0)//既然来释放,那肯定就是已占有状态了。只是为了保险,多层判断!
throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);//释放资源,放弃占有状态
return true;
}
}
// 真正同步类的实现都依赖继承于AQS的自定义同步器!
private final Sync sync = new Sync();
//lock<-->acquire。两者语义一样:获取资源,即便等待,直到成功才返回。
public void lock() {
sync.acquire(1);
}
//tryLock<-->tryAcquire。两者语义一样:尝试获取资源,要求立即返回。成功则为true,失败则为false。
public boolean tryLock() {
return sync.tryAcquire(1);
}
//unlock<-->release。两者语文一样:释放资源。
public void unlock() {
sync.release(1);
}
//锁是否占有状态
public boolean isLocked() {
return sync.isHeldExclusively();
}
}
2.3 CountDowmLatch
中对AQS的使用
内部Sync继承AQS
构造方法
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
await方法
进行等待直到倒数结束
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)//判断当前剩余倒数数量是否大于0
doAcquireSharedInterruptibly(arg);//获取锁不成功 把当前线程放入阻塞队列并阻塞 AQS中方法
}
//tryAcquireShared被CountDownLatch中的Sync重写
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
任务分为N个子线程去执行,state
也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()
一次,state
会CAS
减1。等到所有子线程都执行完后(即state=0
),会unpark()
主调用线程,然后主调用线程就会从await()
函数返回,继续后余动作
2.4 Semaphor
中对AQS的使用
- 在
Semaphore
中,state
表示许可证的剩余数量 - 看
tryAcquire
方法,判断nonfairTryAcquireShared
大于等于0的话,代表成功 - 这里会先检查剩余的许可证数量够不够这次需要的,用减法来计算,如果直接不够,那就返回负数,表示失败,如果够了,就用自旋+CAS来改变state的状态,直到改变成功就返回正数;或者是期间如果被其他人修改了导致剩余数量不够 , 那也返回负数代表获取失败
2.5 ReentrantLock
中对AQS的使用
state
初始化为0,表示未锁定状态。A线程lock()
时,会调用tryAcquire()
独占该锁并将state+1
。此后,其他线程再tryAcquire()
时就会失败,直到A线程unlock()
到state=0
(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state
会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state
是能回到零态的
3. 如果你是Doung Lea
上面说了如何使用AQS框架,但是AQS内部的线程阻塞和唤醒究竟应该怎么去实现呢?
3.1 自旋锁
为了去在java层面解决并发的锁的问题,我们可以使用自旋,声明一个锁对象,锁对象中有一个int
类型,初始值为0的state
成员变量标识这个锁是否已经被占用了;想要进行线程同步的线程都来拿这把锁,拿到了就把state
变量置为1,而没有拿到的就去自旋等待
对于上面想法的问题在于,对state
的赋值操作也会发生竞争,所以我们需要一个原子的方法完成对state
的赋值,可以想到使用cas
操作
public class MyReeLock {
volatile int state= 0;//注意是volatile保证可见性
void lock(){
while(!compareAndSwap(0,1)){
//自旋
}
}
void unlock(){
state = 0;
}
boolean compareAndSwap(int expected, int newValue){
//cas操作,修改成功返回true
//使用unsafe对象
}
}
使用
MyReeLock myReeLock = new MyReeLock();
myReeLock.lock();
try {
//do something
}finally {
myReeLock.unlock();
}
3.2 yield+自旋
上面的实现的缺点是耗费cpu资源,得不到锁的线程一直在空转,我们可以让得不到锁的线程让出cpu
public class MyReeLock {
volatile int state= 0;//注意是volatile保证可见性
void lock(){
while(!compareAndSwap(0,1)){
yield();//让出cpu
}
}
void unlock(){
state = 0;
}
boolean compareAndSwap(int expected, int newValue){
//cas操作,修改成功返回true
//使用unsafe对象
}
}
3.3 park+自旋
上面的问题在于yield
只是让线程进入就绪状态下一次可能依旧是它获得CPU,为了让出CPU我们可以使用park方法,既能让出CPU又能被叫醒
在这个方案我们还加入一个队列parkQueue
用来存放没有竞争到锁的线程,方便唤醒
public class MyReeLock {
volatile int state= 0;
Queue<Thread> parkQueue;
void lock(){
while(!compareAndSwap(0,1)){
park();
}
//lock
}
void unlock(){
state = 0;
lock_notify();
}
void lock_notify(){
LockSupport.unpark(parkQueue.poll());
}
void park(){
parkQueue.add(Thread.currentThread());
LockSupport.park();
}
boolean compareAndSwap(int expected, int newValue){
//cas操作,修改成功返回true
}
public static void main(String[] args) {
MyReeLock myReeLock = new MyReeLock();
myReeLock.lock();
try {
//do something
}finally {
myReeLock.unlock();
}
}
}
4. AQS源码
4.1 ReentrantLock
基本实现
上面说到AQS
是一个抽象类,主是是以继承的方式使用。AQS
本身是没有实现任何同步接口的,它仅仅只是定义了同步状态的获取和释放的方法来供自定义的同步组件的使用。从图中可以看出,在java的同步组件中,AQS
的子类(Sync
等)一般是同步组件的静态内部类,即通过组合的方式使用
接下来以ReentrantLock
的源码入手来深入理解下AQS
的实现。在ReentrantLock
类中,有一个Sync
成员变量,是继承了AQS
的子类
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
/**
* Base of synchronization control for this lock. Subclassed
* into fair and nonfair versions below. Uses AQS state to
* represent the number of holds on the lock.
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
...
}
}
这里的Sync
也是一个抽象类,其实现类为FairSync
和NonfairSync
,分别对应公平锁和非公平锁。
ReentrantLock
的提供一个入参为boolean
值的构造方法,来确定使用公平锁还是非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
再看下面源码前我们先回想一下我们自己实现的锁的要点
① 竞争锁通过对一个条件变量的state
的cas操作实现
AbstractQueuedSynchronizer
维护了一个volatile int
类型的变量,用户表示当前同步状态。volatile
虽然不能保证操作的原子性,但是保证了当前变量state
的可见性,compareAndSetState
的实现依赖于Unsafe
的compareAndSwapInt
()方法
② 没有竞争到锁的线程要被park
j.u.c.locks
包提供了LockSupport
类来解决这个问题。方法LockSupport.park
阻塞当前线程直到有个LockSupport.unpark
方法被调用。unpark
的调用是没有被计数的,因此在一个park
调用前多次调用unpark
方法只会解除一个park
操作。另外,它们作用于每个线程而不是每个同步器。一个线程在一个新的同步器上调用park
操作可能会立即返回,因为在此之前可以有多余的unpark
操作。但是,在缺少一个unpark
操作时,下一次调用park
就会阻塞。虽然可以显式地取消多余的unpark
调用,但并不值得这样做。在需要的时候多次调用park
会更高效。park
方法同样支持可选的相对或绝对的超时设置,以及与JVM
的Thread.interrupt
结合 ,可通过中断来unpark
一个线程
③ 被阻塞的线程要放到阻塞队列等待被唤醒
队列就是网上一直在说的CLH(Craig,Landin,and Hagersten)
队列,这是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系(双向链表)。
AQS
是将每一条请求共享资源的线程封装成一个CLH
锁队列的一个结点(Node
),来实现锁的分配
④ 要实现可重入
4.2 看看公平锁
这里以NonfairSync
类为例,看下它的Lock()
的实现:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
加锁成功
lock
方法先通过CAS
尝试将同步状态(AQS
的state
属性)从0修改为1。若直接修改成功了,则将占用锁的线程设置为当前线程。看下compareAndSetState()
和setExclusiveOwnerThread()
实现:
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
可以看到compareAndSetState
底层其实是调用的unsafe
的CAS
系列方法。
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
exclusiveOwnerThread
属性是AQS
从父类AbstractOwnableSynchronizer
中继承的属性,用来保存当前占用同步状态的线程
所以你能看到在单个线程使用AQS技术加锁,或者是多个线程交替使用只涉及到了一个CAS操作,和队列根本无关,而且是在java层面不涉及OS,这也就是为什么jdk1.6的时候CAS一出现就打败了重量级锁Synchronized
加锁失败
如果CAS
操作未能成功,说明state
已经不为0,此时继续acquire(1)
操作,这个acquire()
由AQS实现提供:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
代码很短,不太好了理解,转换下写法(代码1):
public final void acquire(int arg) {
boolean hasAcquired = tryAcquire(arg);
if (!hasAcquired) {
Node currentThreadNode = addWaiter(Node.EXCLUSIVE);
boolean interrupted = acquireQueued(currentThreadNode, arg);
if (interrupted) {
selfInterrupt();
}
}
}
tryAcquire
方法尝试获取锁,如果成功就返回,如果不成功,则把当前线程和等待状态信息构适成一个Node节点,并将结点放入同步队列的尾部。然后为同步队列中的当前节点循环等待获取锁,直到成功。
首先看tryAcquire(arg)
在NonfairSync
中的实现(这里arg=1):
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
首先获取AQS的同步状态(state
),在锁中就是锁的状态,如果状态为0,则尝试设置状态为arg(这里为1), 若设置成功则表示当前线程获取锁,返回true。这个操作外部方法lock()
就做过一次,这里再做只是为了再尝试一次,尽量以最简单的方式获取锁
如果状态不为0,再判断当前线程是否是锁的owner(即当前线程在之前已经获取锁,这里又来获取),如果是owner
, 则尝试将状态值增加acquires
,如果这个状态值越界,抛出异常;如果没有越界,则设置后返回true。这里可以看非公平锁的涵义,即获取锁并不会严格根据争用锁的先后顺序决定。这里的实现逻辑类似synchroized
关键字的偏向锁的做法**,即可重入而不用进一步进行锁的竞争,也解释了ReentrantLock
中Reentrant
的意义**
如果状态不为0,且当前线程不是owner
,则返回false
回到上面的代码1,tryAcquire
返回false
,接着执行addWaiter(Node.EXCLUSIVE)
,这个方法创建结点并入队
看一下AQS队列结构
AQS(AbstractQueuedSynchronizer)类的设计主要代码(具体参考源码)
private transient volatile Node head; //队首 其实队首Node的Thread属性永远为空,看下面的代码实现
private transient volatile Node tail;//尾
private volatile int state;//锁状态,加锁成功则为1,重入+1 解锁则为0
如果是你来实现一个队列来对线程进行排队和管理,你需要关心什么信息呢?
① 线程,肯定要知道我是哪个线程(因为连哪个线程都不知道,你还排啥队,管理个球球?)
② 队列中线程状态,既然知道是哪一个线程,肯定还要知道线程当前处在什么状态,是已经取消了“获锁”请求,还是在“”等待中”,或者说“即将得到锁”
③ 前驱和后继线程,因为是一个等待队列,那么也就需要知道当前线程前面的是哪个线程,当前线程后面的是哪个线程(因为当前线程释放锁以后,理当立马通知后继线程去获取锁)
Node类的设计
public class Node{
volatile Node prev;
volatile Node next;
volatile Thread thread;//该变量类型为Thread对象,表示该节点的代表的线程
int waitStatus;//该int变量表示线程在队列中的状态
//CANCELLED:值为1,表示线程的获锁请求已经“取消”
//SIGNAL:值为-1,表示该线程一切都准备好了,就等待锁空闲出来给我
//CONDITION:值为-2,表示线程等待某一个条件(Condition)被满足
//PROPAGATE:值为-3,当线程处在“SHARED”模式时,该字段才会被使用上
}
再来看一下入队操作
private Node addWaiter(Node mode) {
//由于AQS队列当中的元素类型为Node,故而需要把当前线程tc封装成为一个Node对象,下文我们叫做nc
Node node = new Node(Thread.currentThread(), mode);
//tail为队尾,赋值给pred
Node pred = tail;
//判断pred是否为空,其实就是判断队尾是否有节点,其实只要队列被初始化了队尾肯定不为空,假设队列里面只有一个元素,那么队尾和队首都是这个元素
//换言之就是判断队列有没有初始化
//上面我们说过代码执行到这里有两种情况,1、队列没有初始化和2、队列已经初始化了
//pred不等于空表示第二种情况,队列被初始化了,如果是第二种情况那比较简单
//直接把当前线程封装的nc的上一个节点设置成为pred即原来的对尾
//继而把pred的下一个节点设置为当nc,这个nc自己成为对尾了
if (pred != null) {
//直接把当前线程封装的nc的上一个节点设置成为pred即原来的对尾
node.prev = pred;
//这里需要cas,因为防止多个线程加锁,确保nc入队的时候是原子操作
if (compareAndSetTail(pred, node)) {
//继而把pred的下一个节点设置为当nc,这个nc自己成为对尾了 对应第11行注释
pred.next = node;
//然后把nc返回出去
return node;
}
}
//如果上面的if不成立就会执行到这里,表示第一种情况队列并没有初始化
enq(node);
//返回nc
return node;
}
enq(node)
方法,从字面可以看出这是一个入队操作,来看下具体入队细节
private Node enq(final Node node) {//这里的node就是当前线程封装的node也就是nc
//死循环
for (;;) {
//队尾复制给t,上面已经说过队列没有初始化,故而第一次循环t==null(因为是死循环,因此强调第一次,后面可能还有第二次、第三次,每次t的情况肯定不同)
Node t = tail;
//第一次循环成立
if (t == null) { // Must initialize
//new Node就是实例化一个Node对象下文我们成为nn,调用无参构造方法实例化出来的Node里面三个属性都为null
//入队操作--compareAndSetHead继而把这个nn设置成为队列当中的头部,cas防止多线程、确保原子操作;记住这个时候队列当中只有一个,即nn
if (compareAndSetHead(new Node()))
//这个时候AQS队列当中只有一个元素,即头部=nn,所以为了确保队列的完整,设置头部等于尾部,即nn即是头也是尾,而且是一个成员变量是为null的node,这点记不记得之前说的对队头的thread一直为null
//然后第一次循环结束
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
//为了方便 第二次循环我再贴一次代码来对第二遍循环解释
private Node enq(final Node node) {//这里的node就是当前线程封装的node也就是nc
//死循环
for (;;) {
//对尾复制给t,由于第二次循环,故而tail==nn,即new出来的那个node
Node t = tail;
//第二次循环不成立
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
//不成立故而进入else
//首先把nc,当前线程所代表的的node的上一个节点改变为nn,因为这个时候nc需要入队,入队的时候需要把关系维护好
//所谓的维护关系就是形成链表,nc的上一个节点只能为nn,这个很好理解
node.prev = t;
//入队操作--把nc设置为队尾,队首是nn,
if (compareAndSetTail(t, node)) {
//上面我们说了为了维护关系把nc的上一个节点设置为nn
//这里同样为了维护关系,把nn的下一个节点设置为nc
t.next = node;
//然后返回t,即nn,死循环结束,这个返回其实就是为了终止循环,返回出去的t,没有意义
return t;
}
}
}
}
//这个方法已经解释完成了
enq(node);
//返回nc,不管哪种情况都会返回nc;到此addWaiter方法解释完成
return node;
}
方法体是一个死循环,本身没有锁,可以多个线程并发访问,假如某个线程进入方法,此时head
, tail
都为null
, 进入if(t==null)
区域,从方法名可以看出这里是用CAS的方式创建一个空的nn作为头结点,因为此时队列中只一个头结点,所以tail也指向它,第一次循环执行结束。注意这里使用CAS是防止多个线程并发执行到这儿时,只有一个线程能够执行成功,防止创建多个同步队列。
进行第二次循环时(或者是其他线程enq时),tail不为null,进入else区域。将当前线程的Node结点(简称nc)的prev指向tail,然后使用CAS将tail指向nc。看下这里的实现
private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
expect为t, t此时指向tail,所以可以CAS成功,将tail重新指向CNode。此时t为更新前的tail的值,即指向空的头结点,t.next=node,就将头结点的后续结点指向CNode,返回头结点。经过上面的操作,头结点和CNode的关系如图:
其他线程再插入节点以此类推,都是在追加到链表尾部,并且通过CAS操作保证线程安全。
通过上面分析可知,AQS
的写入是一种双向链表的插入操作,至此addWaiter
分析完毕。
以上入队就完成了,那么接下来要做的是什么? – park
park
addWaiter
返回了插入的节点,作为acquireQueued
方法的入参
入完队要看看这个过程中是不是有线程释放锁了,所以入队了的线程在park前要看看是不是能获得锁,如果能获得就不用park了直接拿锁就好了,这也就是自旋
但是这个自旋不是所以的都自旋,因为这是公平锁,所以只有最前面的才能自旋,才有机会获得锁
final boolean acquireQueued(final Node node, int arg) {//这里的node 就是当前线程封装的那个node 下文叫做nc
//记住标志很重要
boolean failed = true;
try {
//同样是一个标志 打断
boolean interrupted = false;
//死循环
for (;;) {
//获取nc的上一个节点,有两种情况;1、上一个节点为头部;2上一个节点不为头部
final Node p = node.predecessor();
//如果nc的上一个节点为头部,则表示nc为队列当中的第二个元素,为队列当中的第一个排队人;这里的第一和第二不冲突;我上文有解释;
//如果nc为队列当中的第二个元素,第一个排队的则调用tryAcquire去尝试假设---关于tryAcquire看上面的分析
//只有nc为第二个元素;第一个排队的情况下才会尝试加锁,其他情况直接去park了,因为第一个排队的执行到这里的时候需要看看持有有锁的线程有没有释放锁,释放了就轮到我了,就不park了
//有人会疑惑说开始调用tryAcquire加锁失败了(需要排队),这里为什么还要进行tryAcquire不是重复了吗?
//其实不然,因为第一次tryAcquire判断是否需要排队,如果需要排队,那么我就入队;当我入队之后我发觉前面那个人就是第一个,那么我不死心,再次问问前面那个人搞完没有
//如果搞完了,我就不park,接着他搞;如果他没有搞完,那么我则在队列当中去park,等待别人叫我
//但是如果我去排队,发觉前面那个人在睡觉,前面那个人都在睡觉,那么我也睡觉把
if (p == head && tryAcquire(arg)) {
//能够执行到这里表示我来加锁的时候,锁被持有了,我去排队,进到队列当中的时候发觉我前面那个人没有park,前面那个人就是当前持有锁的那个人,那么我问问他搞完没有
//能够进到这个里面就表示前面那个人搞完了;所以这里能执行到的几率比较小;但是在高并发的世界中这种情况真的需要考虑
//如果我前面那个人搞完了,我nc得到锁了,那么前面那个人直接出队列,我自己则是对首;这行代码就是设置自己为对首
setHead(node);
//这里的P代表的就是刚刚搞完事的那个人,由于他的事情搞完了,要出队;怎么出队?把链表关系删除
p.next = null; // help GC
//设置表示---记住记加锁成功的时候为false
failed = false;
//返回false;为什么返回false 为了不调用50行---acquire方法当中的selfInterrupt方法;为什么不调用?下次解释比较复杂
return interrupted;
}
//进到这里分为两种情况
//1、nc的上一个节点不是头部,说白了,就是我去排队了,但是我上一个人不是队列第一个
//2、第二种情况,我去排队了,发觉上一个节点是第一个,但是他还在搞事没有释放锁
//不管哪种情况这个时候我都需要park,park之前我需要把上一个节点的状态改成park状态
//这里比较难以理解为什么我需要去改变上一个节点的park状态呢?每个node都有一个状态,默认为0,表示无状态
//-1表示在park;但是为什么不能自己把自己改成-1状态?为什么呢?因为你得确定你自己park了才是能改为-1;不然你自己改成自己为-1;但是改完之后你没有park那不就骗人?
//所以只能先park;在改状态;但是问题你自己都park了;完全释放CPU资源了,故而没有办法执行任何代码了,所以只能别人来改;故而可以看到每次都是自己的后一个节点把自己改成-1状态
if (shouldParkAfterFailedAcquire(p, node) &&
//改上一个节点的状态成功之后;自己park;到此加锁过程说完了
parkAndCheckInterrupt())//也就是park
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire(Node pred, Node node)
:在我加锁失败一次后要不要再自旋还是直接休眠
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;//前一个的状态,默认值为0,-1表示休眠
if (ws == Node.SIGNAL)//Node.SIGNAL=-1
//前一个在休眠,那我肯定也休眠,不然我能多自旋一次
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);//把前一个的waitStatus改成-1
}
shouldParkAfterFailedAcquire
方法是判断一个争用锁的线程是否应该被阻塞。它首先判断一个节点的前置节点的状态是否为Node.SIGNAL
,如果是,则应当通知它,所以它要阻塞了,返回true
如果前节点的状态大于0,即为CANCELLED
状态时,则会从前节点开始逐步循环找到一个没有被“CANCELLED
”节点设置为当前节点的前节点,返回false。在下次循环执行shouldParkAfterFailedAcquire
时,返回true
。这个操作实际是把队列中CANCELLED
的节点剔除掉
如果shouldParkAfterFailedAcquire
返回了true,则会执行:“parkAndCheckInterrupt
()”方法,它是通过LockSupport.park(this)
将当前线程挂起到WATING
状态,它需要等待一个中断、unpark
方法来唤醒它,通过这样一种FIFO
的机制的等待,来实现了Lock
的操作
再看解锁
先提一句,“不管公平还是非公平模式下,ReentrantLock对于排队中的线程都能保证,排在前面的一定比排在后面的线程优先获得锁”但是,这里有个但是,非公平模式不保证“队列中的第一个线程一定就比新来的(未加入到队列)的线程优先获锁” 因为队列中的第一个线程尝试获得锁时,可能刚好来了一个线程也要获取锁,而这个刚来的线程都还未加入到等待队列,此时两个线程同时随机竞争,很有可能,队列中的第一个线程竞争失败(而该线程等待的时间其实比这个刚来的线程等待时间要久)
既然申请锁的时候会导致线程在得不到锁时被“阻塞”,那么,肯定就是其他线程在释放锁时“唤醒”被阻塞着的线程去“拿锁”
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
其中,unparkSuccessor(h)方法就是“唤醒操作”,主要流程如代码所示
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//如果head节点的下一个节点它是null或者已经被cancelled了(status>0)
//那么就从队列的尾巴往前找,找到一个最前面的并且状态不是cancelled的线程
//至于为什么要从后往前找,不是从前往后找,谁能跟我说一下,这点我也不知道为什么
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//将找到的队列中符合条件的第一个线程“唤醒”
if (s != null)
LockSupport.unpark(s.thread);
}
① 尝试释放当前线程持有的锁
② 如果成功释放,那么去唤醒头结点的后继节点(因为头节点head是不保存线程信息的节点,仅仅是因为数据结构设计上的需要,在数据结构上,这种做法往往叫做“空头节点链表”。对应的就有“非空头结点链表”)