AQS框架学习
Node节点
状态表示
cancelled:表明当前线程已经放弃锁
signal:表明当前线程正在运行,它后面的线程等着被它唤醒
condition:表明当前线程正在有条件的等待
propagate:表示下一次共享状态获取将会传递给后继结点获取这个共享同步状态。
两种模式
- SHARED ,共享模式,一个空的Node对象
- EXCLUSIVE,独占模式,直接是null
等待队列
AQS提供一个等待队列,该队列是一个双向链表结构,用来接收为获取到锁的线程所生成的node,该队列的head和tail都是同一个空的Node实例,并且都是懒加载。该队列所有的操作都是基于CAS的,所以可以保证线程安全性,每次添加新的node节点都是添加在链表的尾部。
独占模式
获取锁
如果获取锁失败且将当前线程添加到等待队列失败,则中断当前线程,代码如下:
当然,尝试获取锁在AQS框架里是一个抽象方法,让其子类来实现。独占模式下,Node节点的nextWaiter为null,在将当前线程添加到队列时,需要检查队列是不是还没有初始化。如果队列是有效的,则将当前线程所生成的节点添加到队列的尾部。当当前线程所生成的node节点处于排队期的时候,当前线程就开始以独占形式进行自旋去不断地尝试获取锁,因为尝试获取锁是在排队之前执行的,所以这样不能保证公平性,新的线程很可能不需要排队直接获得锁这对于在排队的线程们来说是不公平的,所以就需要尝试获取锁的这个方法,也就是tryAcquire来维持公平性,比如判断线程的上一个节点是不是head节点,不是的话直接返回fasle(参考道格·李老爷子的论文)。
添加到阻塞队列addWaiter
AQS再添加队列时采用CAS自旋的方式去添加线程到阻塞队列的队尾
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
private Node enq(final Node node) {
//AQS自旋添加到队列尾部,保证线程安全性
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
线程自旋:acquireQueued
只要当前线程获取锁失败,就清除当前线程的排队状态。判断是否结束自旋有两个条件,首先当前线程所生成的node节点的上一个节点必须是head节点,其次tryAcquire方法返回true,如果当前线程获得锁,则设置head节点为当前节点,因为head节点的next只有一个,所以在条件通过的情况下设置head节点是线程安全的。如果条件不满足自旋的停止条件,需要判断当前线程的上一个线程节点的状态waitStatus是不是SIGNAL,如果是SIGNAL,则说明当前线程的上一个节点已经释放了锁,所以当前线程可以先阻塞,不再进行自旋,等待它上一个节点的唤醒
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)) {
//因为head节点的next只有一个,所以在条件通过的情况下设置head节点这个操作是线程安全的。
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//每次尝试获取锁失败之后,当前线程都有机会使自己阻塞等待,不需要继续自旋
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//如果自旋途中出现异常,或者自旋失败,则设置当前线程节点的状态为CANCELLED,表明当前线程放弃阻塞
if (failed)
cancelAcquire(node);
}
}
private void setHead(Node node) {
//head节点释放掉thread的引用,是因为可能当前线程执行完成后就被回收了
head = node;
node.thread = null;
node.prev = null;
}
通过shouldParkAfterFailedAcquire方法的逻辑可以知道,线程在没有获得锁时,也并不是一直自旋,而是每次获取锁失败后,都尝试去阻塞自身,而阻塞条件就是当前线程节点的上一个节点状态为SIGNAL,也就是等待前面那个线程唤醒自己,如果是SIGNAL,则直接阻塞当前线程,如果不是返回false,并尝试着去修改上一个节点的状态为SIGNAL,怎么理解呢,就好比是用上一个节点的状态来决定当前节点现在的行为,所以每次自旋,当前线程都通过CAS去修改当前线程节点上一个节点的状态为SIGNAL,以此来希望当前节点在下次自旋时可以阻塞等待被唤醒,而不是一直自旋
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
//如果上一个节点的状态为SIGNAL,则说明当前节点不需要再去自旋尝试获取锁了,直接阻塞自己,等待唤醒
if (ws == Node.SIGNAL)
return true;
//节点状态只有CANCELLED状态是大于0的,而CANCELLED状态代表放弃竞争锁,所以当上个节点的状态值大于0时,说明当前节点的上个节点已经放弃竞争,则当前节点不需要再去关联它,类似的说,节点的waitStatus决定者下一个节点的动作,所以当waitStatus大于1时,它已经不应该出现在阻塞队列上了(换句话说它不配)
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//采用CAS尝试去修改上个节点的状态值为SIGNAL,从而下次自旋时阻塞自身,当然设置失败也没关系,大不了多来几次
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
//只要上个节点状态不是SIGNAL,则返回false,不阻塞自己
return false;
}
小小总结一下,这个阻塞队列如下图所示:
status值必须不大于0,如果大于0,大于0的那个节点就会被剔除掉
释放锁
如果tryRelease通过,判断head节点是否为null,如果为null则说明队列还没有初始化,则说明目前没有线程在争抢锁,直接返回true,或者当前线程的节点的waitStatus等于0,则说明当前节点是新建的,也返回true。如果以上两种都不满足,则说明有线程在抢占这个锁,则需要唤醒当前节点后面的线程节点,唤醒方法如下:
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
//当前线程节点调用此方法,则说明当前线程需要释放锁,所以如果当前节点的状态时小于0的则设置当前状态为0,以便重复调用时不再进入这个方法
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); //获取下一个节点,并且唤醒它
Node s = node.next;
//如果当前节点的下一个节点next为null或者next的状态是大于0的(已经放弃竞争),则从尾部节点tail到当前节点这个范围内去倒叙查找一个合适的节点去唤醒它,这个合适的节点判断条件:1,节点不等于null。2,节点状态不能是大于1的(放弃竞争的)
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);
}
取消竞争锁
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设置为上一个节点pred
if (node == tail设置为 && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
int ws;
//如果上一个节点pred不是头部节点head,并且含有的线程不是null,在满足以下情况时将上一个he下一个节点建立链接
//1,上一个几点状态是SIGNAL(说明上一个节点正在执行,所以不需要要求线程必须不为null)
//2,上一个节点不大于0且CAS设置它的状态为成功
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
//说明上一个节点是head,则唤醒当前节点后面的节点,因为能调用当前方法,说明当前线程已经获得锁,不然当前线程可能是处于自选状态或者阻塞状态(绝多数情况是阻塞)
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
共享模式
获取锁
同样的tryAcquireShared方法留个子类实现,不同的是,返回值是一个int值,当返回值大于0时认为获取锁成功,换句话说,只要返回值大于0就能获取锁,那么锁就可以有多把,截止锁发完之前,其他线程都可以获取这个锁,从而达到共享的目的。只要tryAcquireShared方法返回值小于0,则说明当前的锁已经发完了,需要将当前线程阻塞。
doAcquireShared方法在获取锁失败后执行,将当前线程添加到阻塞队列,看代码,其实会发现基本同独占模式基本逻辑一样,唯一区别就在于当前线程获取锁之后,不光将自身设置为head节点,还回唤醒当前线程后面的节点,让它尝试获取一次锁,毕竟共享模式下的锁可能有多个,也就是setHeadAndPropagate方法,
private void doAcquireShared(int arg) {
//对象模式为共享
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
//不同与独占模式,这里是要锁的数量大于0就代表可以获取锁
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
setHeadAndPropagate方法,看方法明就可以知道,设置当前节点为head节点,并且唤醒下一个节点
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
/** 唤醒条件:
* 1,propagate也就是锁的数量大于0
* 2,头部节点的状态不是新建状态和放弃状态,因为在队列里的节点状态在自旋过程中都会被修改成小于0的值,新建的节点状态会被修改成-1也就是SIGNAL,二大于0的节点会被删除
* 3,h == null,(h = head) == null 我个人理解可能就是判断一些NPE,其他的用途如果以后有新的理解会继续补充进来
*/
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
//如果s == null说明当前阻塞队列上并没有等待的线程,但是为什么要继续唤醒操作呢?,个人理解,因为在你调用setHead(node)后,因为head节点的改变,所以其他的等待线程也有可能被唤醒,所以即使当前的node节点的next为null,但是新的 以当前节点为prev的节点 的next 或许不是null,所以唤醒一下,此举或许可以提高系统的性能
if (s == null || s.isShared())
doReleaseShared();
}
}
doReleaseShared方法
private void doReleaseShared() {
//如果head节点为null(未初始化,代表着没有等待的节点,比如没有调用加锁方法直接调用释放所的方法)
//或者head==tail(代表着初始化了,但是没有等待节点)则不做唤醒操作
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
//waitStatus等于SIGNAL则说明 下个节点已经阻塞并等待着被唤醒,则修改状态为初始状态并唤醒下个节点,如果waitStatus等于0也就是初始化节点,说明下一个节点不需要被唤醒或者已经被唤醒,则修改状态为传播状态PROPAGATE
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
//头部节点可能会改变,所以头部节点发生改变,说明现在可能有节点等待着唤醒,则继续判断唤醒
if (h == head) // loop if head changed
break;
}
}
释放锁
同独占模式
小结:
AQS核心组件:一个CAS操作的阻塞队列、一个volatile修饰的int值state、一个线程阻塞工具LockSupport,但是AQS框架里并没有使用这个state,这个state留给子类去使用,配合AQS预留给子类实现的方法tryAcquire和tryRelease来实现一个Java语言层面的管程。
对于队列来说,每次新的节点插入都采用的是尾插法,且都是采用CAS机制。队列一开始为null,使用之前必须初始化,初始化后的队列是一个head和tail节点相同的的空Node实例,且head节点一但被初始化,在使用过程中必不可能变成null。
另外就是AQS提供了共享和独占两种形式的锁的支持。对于独占模式,锁只有一把谁拿到谁执行,拿不到的都将在阻塞队列的等待,每个等待的节点理论上都等待着被上一个节点唤醒,但如果上一个节点放弃竞争,那么它将等待上一个节点的上一个节点(知道找到没有放弃的)唤醒。每个等待的节点再被插入到队列后会立即去尝试获取一次锁,因为我们总希望一个锁被持有的时间不会太长,可能很快就释放了,这也是提高系统吞吐量的优化,如果获取失败则开始自旋,这个自旋不是一直执行的,它自选的目的是为了修改它上一个节点的状态值status,将它修改成SIGNAL,如果设置成功表明自己已经准备好被前面的节点唤醒同时阻塞自己。当节点被唤醒后,会将自身节点插入到head节点的位置,并且释放持有的prev和thread引用,便于gc。对于共享模式,锁可能有多把,所以同时可以有多个线程执行。阻塞流程是和独占模式相同的,但是不同的是,独占模式的节点唤醒只能是被动型性的,换句话说,只能依赖用户去调用release,而共享模式不同,当当前的节点被唤醒后,除了执行自己原有和独占模式相同的逻辑外,会额外去尝试唤醒当前节点的下一个节点,可以理解为蔓延唤醒,这么做的原因,个人理解是共享模式下的锁有多个,所以线程获取到的锁的可能性大大提高,唤醒当前节点需要一把锁但是很可能此时空闲的锁不止只有一把,所以每次都是积极的去尝试唤醒下一个节点,也就是说,共享模式的节点唤醒是主动性的。