AQS底层原理与源码解析

介绍AQS AbstractQueuedSynchronizer  抽象队列锁  用来构建锁的框架

  1. state变量
    1. 使用int类型的volatile变量维护同步状态(state)
    2. 围绕state提供锁的两种操作“获取”和“释放=0”; 读锁与写锁区分 65535= 2^16-1
    3. FutureTask用它来表示任务的状态
  2. 内置的同步队列 CLH,双端双向列表
    1. FIFO队列存放阻塞的等待线程,来完成线程的排队执行;
    2. 封装成Node,Node维护一个prev引用和next引用,实现双向链表
    3. AQS维护两个指针,分别指向队列头部head和尾部tail
  3. 功能:
  4.  

锁是面向使用者的,定义了用户调用的接口,隐藏了实现细节;

AQS是锁的实现者,屏蔽了同步状态管理,线程的排队,等待唤醒的底层操作。

锁是面向使用者,AQS是锁的具体实现者

背后复杂的线程排队,线程阻塞/唤醒,如何保证线程安全,都由AQS为我们完成了

分为独占锁、共享锁

继承AbstractQueuedSynchronizer并重写指定的方法

独占式如ReentrantLock,共享式如Semaphore,CountDownLatch

 

1.要实现一个独占锁,那就去重写或封装tryAcquire,tryRelease方法,

Acquire:tryAcquire(arg)、addWaiter【入队】、acquireQueued【循环获取锁,失败则挂起shouldParkAfterFailedAcquire】

Release:tryRelease(arg)、检查waitStatus、unparkSuccessor(h)【;//唤醒后继结点】

 

2.要实现共享锁,就去重写tryAcquireShared,tryReleaseShared

acquireShared:tryAcquireShared、doAcquireShared(arg)【setHeadAndPropagate】;【尝试获取(state+1),入队、等待唤醒】

realseShared:tryRealseShared、doReleaseShared()【CAS下的 unparkSuccessor(h)】;【state-1并判断、唤醒后继】

 

等待状态位(waitStatus)【等待模式】

CANCELLED 1:因为超时或者中断,结点被设置为取消状态,不能去竞争锁,不能转换为其他状态;会被检测并踢出队列,被GC回收;

SIGNAL -1:表示这个结点的继任结点被阻塞,到时需要唤醒它

CONDITION -2:表示这个结点在条件队列中,因为等待某个条件而被阻塞;

共享传播状态

PROPAGATE -3:使用在共享模式头结点有可能牌处于这种状态,表示锁的下一次获取可以无条件传播;

 

闭锁(CountDownLatch) 源码,是一种共享锁

N个线程调用await阻塞在for循环里面,然后N个线程依次调用countDown,每调用一次state减1,直接state为0,这些线程退出for循环(解除阻塞)!

退出for循环时,由于头结点状态标志位为PROPAGATE,而且这些结点都是共享模式,由头结点一传播,这些结点都获取锁,于是齐头并进执行了......

 

  1. public void await() throws InterruptedException;
  2. public void countDown();

 

countDown()方法调用 releaseShared(int arg),直到state-1后为0.开始执行传递唤醒后继shared线程

void countDown{

if (tryReleaseShared(arg)) { // state-1后是否为 0

doReleaseShared(); //检测共享节点和等待状态为 PROPAGATE -3的后继节点,开始唤醒unpark(thread);

return true;

}

}

 

await方法使当前线程一直等待,阻塞在doAcquireShared方法中,除非线程被中断,或者 state == 0;

当调用countDown方法之后state-1,当锁存器减少到0时,await方法就会返回

 

 

两次doReleaseShared();的区别

  1. setHeadAndPropagate()方法表示等待队列中的线程成功获取到共享锁,这时候它需要唤醒它后面的共享节点的线程
  2. 但是当通过releaseShared()方法去释放一个共享锁的时,接下来等待独占锁跟共享锁的线程都可以被唤醒进行尝试获取锁

 

 

独占锁

Acquire:tryAcquire(arg)、addWaiter【线程入队】、acquireQueued【循环获取锁,失败则挂起shouldParkAfterFailedAcquire,等待头结点唤醒】

Release:tryRelease(arg)【每次state-1】、unparkSuccessor(h)【若state==0,唤醒后继结点】

获取acquire(int arg)

 

a.首先,调用使用者重写的tryAcquire方法,若返回true,意味着获取同步状态成功,后面的逻辑不再执行;若返回false,也就是获取同步状态失败,进入b步骤;

b.此时,获取同步状态失败,构造独占式同步结点,通过addWatiter将此结点添加到同步队列的尾部(此时可能会有多个线程结点试图加入同步队列尾部,需要以线程安全的方式添加);

c.该结点以在队列中尝试获取同步状态,若获取不到,则阻塞结点线程,直到被前驱结点唤醒或者被中断

 

public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();//请求锁成功,加入等待队列,中断自己,等待被唤醒 }

 

tryAcquire(arg)会调用nonfairTryAcquire(1),含插队

获取当前线程

获取并检查锁状态,若c==0,CAS将state置为1,并设置当前线程获取独占锁,返回true

else,查看当前线程是不是已经是独占锁。若是,state+1,返回true

都不是,返回false

final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread();//获取当前线程 int c = getState();//获取锁数量 if (c == 0) {//如果锁数量为0,证明该独占锁已被释放,当下没有线程在使用 if (compareAndSetState(0, acquires)) {//继续通过CAS将state由0变为1,注意这里传入的acquires为1; setExclusiveOwnerThread(current);//将当前线程设置为独占锁的线程 return true; } } else if (current == getExclusiveOwnerThread()) {//查看当前线程是不是就是独占锁的线程 int nextc = c + acquires;//如果是,锁状态的数量为当前的锁数量+1 if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc);//设置当前的锁数量 return true; } return false; }

若请求锁失败,addWaiter 将当前线程链入队尾并挂起,之后等待被唤醒 【快速、正常】

  1. 首先使用addWaiter(Node.EXCLUSIVE)将当前线程封装进Node节点,然后将该节点加入等待队列
    1. 先快速入队:存在尾节点,将使用CAS尝试将尾节点设置为node】
    2. 如果快速入队不成功【尾节点为空】,使用正常入队方法enq,【无限循环=第一次阻塞】:创建一个dummy节点,并将该节点通过CAS设置到头节点】【期间多线程:若头结点不为null,执行快速入队到尾节点】

acquireQueued 入队成功后,返回node节点,继续第三次插队

无限循环调用:获取node的前驱节点p

p==head&&tryAcquire(1) 是唯一跳出循环的方法:p成为头结点并且获取锁成功:如果p是头节点,就继续使用tryAcquire(1)方法插队,若成功,不用中断,第三次插队成功;【只有前驱结点是头结点的结点,也就是老二结点,才有机会去tryAcquire;】

  1. 如果p不是头节点,或者tryAcquire(1)请求不成功,执行shouldParkAfterFailedAcquire(Node pred, Node node)来检测当前节点是不是可以安全的被挂起:判断前驱节点p的等待状态waitStatus
    1. SIGNAL(即可以唤醒下一个节点的线程),则node节点的线程可以安全挂起,返回true
    2. CANCELLED,则p的线程被取消了,我们会将p之前的连续几个被取消的前驱节点从队列中剔除
    3. 等待状态是除了上述两种的其他状态,CAS尝试将前驱节点的等待状态设为SIGNAL【p与node竞争】,返回true

 

挂起后后 跳出循环,需要中断自身

LockSupport.park(this);return Thread.interrupted(); //挂起当前的线程,后等待前驱节点unpark唤醒该线程;唤醒方法为public

 

释放 release(int arg)

 

//释放锁的操作

public final boolean release(int arg)

//尝试释放锁,若释放后state==0,成功,唤醒后继节点;否则失败直接返回false;若成功,判断waitStatus不等于0,唤醒后继节点 if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }

 

调用tryRelease释放锁,如果释放失败,直接返回

protected final boolean tryRelease(int releases) {

//获取state值,释放一定值 int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false;

//如果差是0,表示锁已经完全释放 if (c == 0) { free = true;

//下面设置为null表示当前没有线程占用锁 setExclusiveOwnerThread(null); }

//如果c不是0表示锁还没有完全释放,修改state值 setState(c); return free; }

 

释放锁成功后需要唤醒继任结点,是通过方法unparkSuccessor实现

若后继结点为空或处于CANCEL状态,从后向前遍历找寻一个正常的结点,唤醒其对应线程

private void unparkSuccessor(Node node) { //传进来头结点

int ws = node.waitStatus;

if (ws < 0)//检查头结点的waitStatus位,小于0表示没被取消

compareAndSetWaitStatus(node, ws, 0);//将当前节点的状态修改为0

 

Node s = node.next;

if (s == null || s.waitStatus > 0) {

s = null;

//从尾向头寻找,tail开始,寻找pre

for (Node t = tail; t != null && t != node; t = t.prev)

if (t.waitStatus <= 0)

s = t;

}

//查看头结点的下一个结点,如果下一个结点不为空,且waitStatus<=0,表示后继结点没有被取消

if (s != null)

LockSupport.unpark(s.thread);

}

 

这里并没有从头向尾寻找,而是尾向头寻找,为什么

因为在CLH队列中的结点随时有可能被中断,被中断的结点的waitStatus设置为CANCEL,而且它会被踢出CLH队列,如何个踢出法,就是它的前趋结点的next并不会指向它,而是指向下一个非CANCEL的结点,而它自己的next指针指向它自己。一旦这种情况发生,如何从头向尾方向寻找继任结点会出现问题,因为一个CANCEL结点的next为自己,那么就找不到正确的继任接点

CANCEL结点的next指针为什么要指向它自己,为什么不指向真正的next结点?为什么不为NULL?

第一个问题的答案是这种被CANCEL的结点最终会被GC回收,如果指向next结点,GC无法回收

第二个问题的回答,为了使isOnSyncQueue方法更新简单,判断一个结点是否在同步队列中,指向它自己时表示在,可以踢出

如果一个结点next不为空,那么它在同步队列中,如果CANCEL结点的后继为空那么CANCEL结点不在同步队列中,这与事实相矛盾。

因此将CANCEL结点的后继指向它自己是合理的选择。

 

共享锁:

acquireShared:tryAcquireShared、doAcquireShared(arg)【setHeadAndPropagate】;

realseShared:tryRealseShared、doReleaseShared()【unparkSuccessor(h)】;

 

  1. 共享式:共享式地获取同步状态
  2. 对于独占式同步组件来讲,同一时刻只有一个线程能获取到同步状态,其他线程都得去排队等待,其待重写的尝试获取同步状态的方法tryAcquire返回值为boolean,这很容易理解;
  3. 对于共享式同步组件来讲,同一时刻可以有多个线程同时获取到同步状态,这也是“共享”的意义所在。其待重写的尝试获取同步状态的方法tryAcquireShared返回值为int。

lock,调用acquireShared

public void lock() {

sync.acquireShared(1);

}

//获取共享锁API acquireShared

public final void acquireShared(int arg) {

//state != 0时,tryAcquireShared(arg) < 0,才会真正操作锁;表示获取锁失败

if (tryAcquireShared(arg) < 0)

doAcquireShared(arg);

}

 

tryAcquireShared(arg) 判断是否需要唤醒后续节点获取共享锁

tryAcquireShared(arg):return (getState() == 0) ? 1 : -1;返回state是否为0

protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; }

< 0:表示state != 0,获取锁失败,执行doAcquireShared(arg)

//= 0:表示当前线程获取共享锁成功,但不需要把它后面等待的节点唤醒

> 0:state==0,表示当前线程获取共享锁成功,且此时需要把后续节点唤醒让它们去尝试获取共享锁

获取锁失败后 操作doAcquireShared(int arg) ,里面继续循环尝试获取共享锁,若成功,可以传播状态;失败则入队挂起

大体逻辑与独占式的acquireQueued差距不大,只不过由于是共享式,会有多个线程同时获取到线程,也可能同时释放线程,空出很多同步状态,

所以当排队中的老二获取到同步状态,如果还有可用资源,会继续传播下去。

 

  1. 封装为Node.SHARED,调用addWaiter:根据队列是否为空采用CAS进行快速入队和正常入队,返回头结点
  2. 自旋,检测是否前继节点p为head,是则再次调用tryAcquireShared(arg),尝试获取,若state==0,
  3. 调用setHeadAndPropagate,根据s.isShared()标识,调用doReleaseShared()
    1. 作用:当排队中的老二获取到同步状态,如果还有可用资源[waitStatusj和共享标识],会继续传播下去

 

释放共享锁ReleaseShared(int arg)

tryReleaseShared(arg):释放共享锁,state-1

state-1后,返回是否为0;相当于一个栅栏,只有当state==0,继续执行doReleaseShared()

if (tryReleaseShared(arg)) {//state为0时,返回true(针对CountDownLatch)

doReleaseShared();

return true;

}

doReleaseShared(); 调用unparkSuccessor(h);传递唤醒阻塞的共享线程

死循环,共享模式,持有同步状态的线程可能有多个,采用循环CAS保证线程安全

释放同步状态也是多线程的,此处采用了CAS自旋来保证

private void doReleaseShared() { for (;;) {//死循环,共享模式,持有同步状态的线程可能有多个,采用循环CAS保证线程安全 Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; unparkSuccessor(h);//唤醒后继结点 } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; } if (h == head) break; } }

 

1)共享锁初始化时会给state设值,所有请求锁的共享节点都会放入SyncQueue中阻塞

2)一个节点A获取锁(成为head节点)之后,会唤醒它的下一个共享节点线程B,B唤醒后会去竞争锁,一直往下,直到后面的共享节点都唤醒为止。

此时所有共享节点都获取了锁,都可以往下执行了

 

如果state值代表的许可数足够使用,那么请求线程将会获得同步状态即对共享资源的访问权,并更新state的值(一般是对state值减1),但如果state值代表的许可数已为0,则请求线程将无法获取同步状态,线程将被加入到同步队列并阻塞,直到其他线程释放同步状态(一般是对state值加1)才可能获取对共享资源的访问权

 

acquireShared()方法获取锁

  1. 当tryAcquireShared(arg)返回值>=0时(可以在重写该方法时自定义锁的数量),表示获取锁成功,不会进入doAcquireShared。
  2. 当tryAcquireShared(arg)返回值<0时,线程都锁完,等待被唤醒,进入doAcquireShared(arg)方法,封装Node.SHARED节点放入等待队列,并自旋阻塞

 

private void doAcquireShared(int arg) {

final Node node = addWaiter(Node.SHARED);//构造一个共享结点,添加到同步队列尾部。若队列初始为空,先添加一个无意义的dummy结点,再将新节点添加到队列尾部

boolean failed = true;//是否获取成功

try {

boolean interrupted = false;//线程parking过程中是否被中断过

for (;;) {//自旋

final Node p = node.predecessor();//获取前驱结点

if (p == head) {//头结点持有同步状态,只有前驱是头结点,才有机会尝试获取同步状态

int r = tryAcquireShared(arg);//尝试获取同步资源

if (r >= 0) {//r>=0,获取成功

setHeadAndPropagate(node, r);//获取成功就将当前结点设置为头结点,若还有可用资源,传播下去,也就是继续唤醒后继结点,即doReleaseShared();

p.next = null; // 方便GC

if (interrupted)

selfInterrupt();

failed = false;

return;

}

}

if (shouldParkAfterFailedAcquire(p, node) &&//是否能安心进入parking状态

parkAndCheckInterrupt())//阻塞线程

interrupted = true;

}

} finally {

if (failed)

cancelAcquire(node);

}

}

 

//setHeadAndPropagate 把node节点设置成head节点,且node.waitStatus->Node.PROPAGATE*

private void setHeadAndPropagate(Node node, int propagate) {

Node h = head;//h用来保存旧的head节点

setHead(node);//head引用指向node节点

/* 这里意思有两种情况是需要执行唤醒操作

* 1.propagate > 0 表示调用方指明了后继节点需要被唤醒

* 2.头节点后面的节点需要被唤醒(waitStatus<0),不论是老的头结点还是新的头结点*/

if (propagate > 0 || h == null || h.waitStatus < 0 ||

(h = head) == null || h.waitStatus < 0) {

Node s = node.next;

if (s == null || s.isShared())//node是最后一个节点或者 node的后继节点是共享节点

/* 如果head节点状态为SIGNAL,唤醒head节点线程,重置head.waitStatus->0

* head节点状态为0(第一次添加时是0),设置head.waitStatus->Node.PROPAGATE表示状态需要向后继节点传播

*/

doReleaseShared();//对于这个方法,其实就是把node节点设置成Node.PROPAGATE状态

}

}

 

区分读锁和写锁

tryAcquire 函数是尝试获取写锁:1.如果有读线程或者写线程且不是当前线程,直接失败;2.如果写锁的count超过了65535,直接失败

 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值