靓仔靓女们好,咱们又见面了,我是公众号:java小杰要加油,现就职于京东,致力于分享java相干常识,包含但不限于并发、多线程、锁、mysql以及京东面试真题
AQS介绍
AQS全称是AbstractQueuedSynchronizer,是一个形象队列同步器,JUC并发包中的大部分的并发工具类,都是基于AQS实现的,所以了解了AQS就算是四舍五入把握了JUC了(好一个四舍五入学习法)那么AQS到底有什么神奇之处呢?有什么特点呢?让咱们明天就来拔光它,一探到底!
- state:代表被抢占的锁的状态
- 队列:没有抢到锁的线程会包装成一个node节点寄存到一个双向链表中
AQS大略长这样,如图所示:
你说我轻易画的,我可不是轻易画的啊,我是有bear而来,来看下AQS根本属性的代码
那么这个Node节点又蕴含什么呢?来吧,展现。
那么咱们就能够把这个队列变的更具体一点
怎么忽然进去个exclusiveOwnerThread
?还是保留以后取得锁的线程,哪里来的呢
还记得咱们AQS一开始继承了一个类吗
这个exclusiveOwnerThread
就是它外面的属性
再次回顾总结一下,AQS属性如下:
- state:代表被抢占的锁的状态
- exclusiveOwnerThread:以后取得锁的线程
队列:没有抢到锁的线程会包装成一个node节点寄存到一个双向链表中
Node节点 :
* thread: 以后node节点包装的线程 * waitStatus:以后节点的状态 * pre: 以后节点的前驱节点 * next: 以后节点的后继节点 * nextWaiter:示意以后节点对锁的模式,独占锁的话就是null,共享锁为Node()
好了,咱们对AQS大略是什么货色什么构造长什么样子有了个分明的认知,上面咱们间接上硬菜,从源码角度剖析下,AQS加锁,它这个构造到底是怎么变动的呢?
注:以下剖析的都是独占模式下的加锁
- 独占模式 : 锁只容许一个线程取得 NODE.EXCLUSIVE
- 共享模式 :锁容许多个线程取得 NODE.SHARED
AQS加锁源码——acquire
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
乍一看这是什么啊,没关系,咱们能够把它画成流程图不便咱们了解,流程图如下
上面咱们来一个一个剖析,图文并茂,来吧宝贝儿。
AQS加锁源码——tryAcquire
protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
这是什么状况?怎么间接抛出了异样?其实这是由AQS子类重写的办法,就相似lock锁,由子类定义尝试获取锁的具体逻辑
咱们平时应用lock锁时往往如下 (若不想看lock锁怎么实现的能够间接跳转到下一节)
ReentrantLock lock = new ReentrantLock(); lock.lock(); try{ //todo }finally { lock.unlock(); }
咱们看下lock.lock()
源码
public void lock() { sync.lock(); }
这个sync
又是什么呢,咱们来看下lock类的总体属性就好了
所以咱们来看下 默认非偏心锁的加锁实现
static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; final void lock() { //将state状态从0设为1 CAS形式 if (compareAndSetState(0, 1)) //如果设定胜利的话,则将以后线程(就是本人)设为占有锁的线程 setExclusiveOwnerThread(Thread.currentThread()); else //设置失败的话,就以后线程没有抢到锁,而后进行AQS父类的这个办法 acquire(1); } protected final boolean tryAcquire(int acquires) { //调用非偏心锁的办法 return nonfairTryAcquire(acquires); } }
当初压力又来到了nonfairTryAcquire(acquires)
这里
final boolean nonfairTryAcquire(int acquires) { //取得以后线程 final Thread current = Thread.currentThread(); //取得以后锁的状态 int c = getState(); //如果锁的状态是0的话,就表明还没有线程获取到这个锁 if (c == 0) { //进行CAS操作,将锁的状态改为acquires,因为是可重入锁,所以这个数字可能是>0的数字 if (compareAndSetState(0, acquires)) { //将以后持有锁的线程设为本人 setExclusiveOwnerThread(current); //返回 获取锁胜利 return true; } }// 如果以后锁的状态不是0,判断以后获取锁的线程是不是本人,如果是的话 else if (current == getExclusiveOwnerThread()) { //则重入数加acquires (这里acquires是1) 1->2 3->4 这样 int nextc = c + acquires; if (nextc < 0) // overflow 异样检测 throw new Error("Maximum lock count exceeded"); //将锁的状态设为以后值 setState(nextc); //返回获取锁胜利 return true; } //以后获取锁的线程不是本人,获取锁失败,返回 return false; }
由此可见,回到方才的问题,AQS中的tryAcquire
是由子类实现具体逻辑的
AQS加锁源码——addWaiter
如果咱们获取锁失败的话,就要把以后线程包装成一个Node节点,那么具体是怎么包装的呢,也须要化妆师经纪人吗?
咱们来看下源码就晓得了addWaiter(Node.EXCLUSIVE), arg)
这就代表增加的是独占模式的节点
private Node addWaiter(Node mode) { //将以后线程包装成一个Node节点 Node node = new Node(Thread.currentThread(), mode); // 申明一个pred指针指向尾节点 Node pred = tail; //尾节点不为空 if (pred != null) { //将以后节点的前置指针指向pred node.prev = pred; //CAS操作将以后节点设为尾节点,tail指向以后节点 if (compareAndSetTail(pred, node)) { //pred下一节点指针指向以后节点 pred.next = node; //返回以后节点 (此时以后节点就曾经是尾节点) return node; } } //如果尾节点为空或者CAS操作失败 enq(node); return node; }
其中node的构造函数是这样的
Node(Thread thread, Node mode) { // Used by addWaiter this.nextWaiter = mode; this.thread = thread; }
咱们能够通过图解的办法来更直观的来看下addWaiter
做了什么
由图可知,如果已经尾节点不为空的时候,node节点会退出到队列开端,那么如果已经尾节点为空或者CAS失败调用enq(node);
会怎么样呢?
AQS加锁源码——enq
private Node enq(final Node node) { //死循环,直到有返回值 for (;;) { //申明一个t的指针指向tail Node t = tail; //如果尾巴节点为空 if (t == null) { // Must initialize //则CAS设置一个节点为头节点(头节点并没有包装线程!)这也是提早初始化头节点 if (compareAndSetHead(new Node())) //将尾指针指向头节点 tail = head; } else { //如果尾节点不为空,则阐明这是CAS失败 // 将node节点前驱节点指向t node.prev = t; //持续CAS操作将本人设为尾节点 if (compareAndSetTail(t, node)) { //将t的next指针指向本人 (此时本人真的是尾节点了) t.next = node; //返回本人节点的前置节点,队列的倒数第二个 return t; } } } }
- 队列中的头节点,是提早初始化的,加锁时用到的时候才去输入话,并不是一开始就有这个头节点的
- 头节点并不保留任何线程
end 尾分叉
// 将node节点前驱节点指向t node.prev = t; 1 //持续CAS操作将本人设为尾节点 if (compareAndSetTail(t, node)) { 2 //将t的next指针指向本人 (此时本人真的是尾节点了) t.next = node; 3 //返回本人节点的前置节点,队列的倒数第二个 return t; }
咱们留神到,enq
函数有下面三行代码,3是在2执行胜利后才会执行的,因为咱们这个代码无时无刻都在并发执行,存在一种可能就是
1执行胜利,2执行失败(cas并发操作),3没有执行,所以就只有一个线程1,2,3都执行胜利,其余线程1执行胜利,2,3没有执行胜利,呈现尾分叉状况,如图所示
这些分叉失败的节点,在当前的循环中他们还会执行1,直总会指向新的尾节点,1,2,3这么执行,早晚会入队
AQS加锁源码——acquireQueued
final boolean acquireQueued(final Node node, int arg) { // 是否有异样产生 boolean failed = true; try { //中断标记 boolean interrupted = false; //开始自旋,要么以后线程尝试去拿到锁,要么抛出异样 for (;;) { //取得以后节点的上一个节点 final Node p = node.predecessor(); //判断这个节点是不是头节点,如果是头节点则获取锁 if (p == head && tryAcquire(arg)) { //将此节点设为头节点 setHead(node); //原来的头节点出队 p.next = null; // help GC failed = false; //返回是否中断 return interrupted; } // 阐明p不是头节点 // 或者 // p是头节点然而获取锁失败 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) // 中断标记设为true interrupted = true; } } finally { //如果有异样产生的话 if (failed) //勾销以后线程竞争锁,将以后node节点状态设置为cancel cancelAcquire(node); } }
其中有一行代码是setHead(node);
private void setHead(Node node) { head = node; node.thread = null; //将head节点的线程置为空 node.prev = null; }
- 为什么要将头节点的线程置为空呢,是因为在
tryAcquire(arg)
中就曾经记录了以后获取锁的线程了,在记录就多此一举了,咱们看前文中提到的nonfairTryAcquire(acquires)
其中有一段代码
if (compareAndSetState(0, acquires)) { //将以后持有锁的线程设为本人 setExclusiveOwnerThread(current); //返回 获取锁胜利 return true; }
可见 setExclusiveOwnerThread(current);
就曾经记录了取得锁的线程了
咱们acquireQueued
返回值是中断标记,true示意中断过,false示意没有中断过,还记得咱们一开始吗,回到最后的终点
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
如果返回了true,代表此线程有中断过,那么调用 selfInterrupt();
办法,将以后线程中断一下
static void selfInterrupt() { Thread.currentThread().interrupt(); }
AQS加锁源码——shouldParkAfterFailedAcquire
程序运行到这里就阐明
// 阐明p不是头节点// 或者// p是头节点然而获取锁失败if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) // 中断标记设为true interrupted = true;}
咱们来剖析下shouldParkAfterFailedAcquire(p, node)
的源码外面到底做了什么?
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { //获取以后节点的前置节点的状态 int ws = pred.waitStatus; if (ws == Node.SIGNAL) //如果是SIGNAL(-1)状态间接返回true,代表此节点能够挂起 //因为前置节点状态为SIGNAL在适当状态 会唤醒后继节点 return true; if (ws > 0) { //如果是cancelled do { //则从后往前依此跳过cancelled状态的节点 node.prev = pred = pred.prev; } while (pred.waitStatus > 0); //将找到的符合标准的节点的后置节点指向以后节点 pred.next = node; } else { //否则将前置节点期待状态设置为SIGNAL compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
其中的node.prev = pred = pred.prev;
能够看成
pred = pred.prev;node.prev = pred;
可见一顿操作后,队列中跳过了节点状态为cancelled的节点
AQS加锁源码——parkAndCheckInterrupt
当shouldParkAfterFailedAcquire
返回true时就代表容许以后线程挂起而后就执行 parkAndCheckInterrupt()
这个函数
private final boolean parkAndCheckInterrupt() { // 挂起以后线程 线程卡在这里不再下执行,直到unpark唤醒 LockSupport.park(this); return Thread.interrupted(); }
所以以后线程就被挂起啦
AQS加锁源码——cancelAcquire
咱们还记得前文中提到acquireQueued
中的一段代码
try { } finally { if (failed) cancelAcquire(node); }
这是抛出异样时解决节点的代码,上面来看下源代码
private void cancelAcquire(Node node) { //过滤掉有效节点 if (node == null) return; //以后节点线程置为空 node.thread = null; //获取以后节点的前一个节点 Node pred = node.prev; //跳过勾销的节点 while (pred.waitStatus > 0) node.prev = pred = pred.prev; //记录过滤后的节点的后置节点 Node predNext = pred.next; //将以后节点状态改为CANCELLED node.waitStatus = Node.CANCELLED; // 如果以后节点是tail尾节点 则将从后往前找到第一个非勾销状态的节点设为tail尾节点 if (node == tail && compareAndSetTail(node, pred)) { //如果设置胜利,则tail节点前面的节点会被设置为null compareAndSetNext(pred, predNext, null); } else { int ws; //如果以后节点不是首节点的后置节点 if (pred != head && //并且 //如果前置节点的状态是SIGNAL ((ws = pred.waitStatus) == Node.SIGNAL || //或者 //状态小于0 并且设置状态为SIGNAL胜利 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && //并且前置节点线程不为null时 pred.thread != null) { //记录下以后节点的后置节点 Node next = node.next; //如果后置节点不为空 并且后置节点的状态小于0 if (next != null && next.waitStatus <= 0) //把以后节点的前驱节点的后继指针指向以后节点的后继节点 compareAndSetNext(pred, predNext, next); } else { //唤醒以后节点的下一个节点 unparkSuccessor(node); } //将以后节点下一节点指向本人 node.next = node; // help GC } }
看起来太简单了,不过没关系,咱们能够拆开看,其中有这一段代码
//以后节点线程置为空 node.thread = null; //获取以后节点的前一个节点 Node pred = node.prev; //跳过勾销的节点 while (pred.waitStatus > 0) node.prev = pred = pred.prev; //记录过滤后的节点的后置节点 Node predNext = pred.next; //将以后节点状态改为CANCELLED node.waitStatus = Node.CANCELLED;
如图所示
通过while循环从后往前找到signal状态的节点,跳过两头cancelled状态的节点,同时将以后节点状态改为CANCELLED
咱们能够把这简单的判断条件转换成图来直观的看一下
- 以后节点是尾节点时,队列变成这样
- 以后节点是head后继节点
- 以后节点不是尾节点也不是头节点的后继节点(队列中的某个一般节点)
总结
太不容易了家人们,终于到了这里,咱们再来总结一下整体的流程
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
1.基于AQS实现的子类去实现tryAcquire
尝试获取锁
2.如果获取锁失败,则把以后节点通过addWaiter
办法包装成node
节点插入队列
- 如果尾节点为空或者CAS操作失败则调用
enq
办法保障胜利插入到队列,若节点为空则初始化头节点
3.acquireQueued
办法,入队后的节点持续获取锁(此节点的前置节点是头节点)或者挂起
shouldParkAfterFailedAcquire
判断节点是否应该挂起- 如果以后节点的前置节点是signal状态,则返回true,能够挂起
- 如果以后节点的前置节点是cancelled,则队列会从以后节点的前一个节点开始从后向前遍历跳过cacelled状态的节点,将以后节点和非cacelled状态的节点连接起来,返回false,不能够挂起
- 否则将前置节点期待状态设置为SIGNAL,返回false,不能够挂起
parkAndCheckInterrupt
挂起以后线程cancelAcquire
将以后节点状态改为cancelld
selfInterrupt();
设置中断标记,将中断补上
往期精彩举荐
- 京东这道面试题你会吗?
- ?线程池为什么能够复用,我是蒙圈了。。。
- 学会了volatile,你变心了,我看到了
- mysql能够靠索引,而我只能靠打工,加油,打工人!
絮絮叨叨
如果大家感觉这篇文章对本人有一点点帮忙的话
若文章有误欢送指出,靓仔靓女,咱们下篇文章见,扫一扫,关注我,开启咱们的故事