整体认识
自我理解
为了哪些基于FIFO阻塞队列并且依赖于一个int类型状态属性的同步器提供基础的构建框架。这里的重点是
- FIFO阻塞队列
- int类型的状态位
基于该类定制的同步器,要明确的定义状态的意义,也就是0代表什么,1代表什么,还有其它。这里就提供了拓展点,让用户自定义。
并且使用getState()、setState()、compareAndSetState()方法来操作int类型的状态标志属性,并且要重新定义一下方法(这里分类独占模式和共享模式):
- tryAcquire()
- tryRelease()
- tryAcquireShared()
- tryTeleaseShared()
- isHeldExclusively()
该类支持两种模式,独占模式和共享模式,对于该类的子类,没必要支持这两种模式,只要支持其中一种就可以,也就是上面的方法,如果子类要实现独占模式,就没必要重新定义共享模式的方法,只需要重新定义tryAcquire()和tryRelease()和isHeldExclusively()方法。
大神还在注释中列举了两种模式的使用例子,其实基于AQS可以定制自己的同步器。知识Java提供了经典的同步器,Sychronized和ReentrantLock,这两个是独占模式的。对于共享模式的,有CountDownLatch等。在日常情况下是够用的,如果不能满足业务要求,就自己基于AQS定制同步器。
关键点
对于一些字段的状态进行修改,使用的是CAS原子操作,CAS在计算机指令来说就是一条指令。
首先要搞懂一点,队列是一个双向队列的FIFO的队列,关于队列,就是尾结点插入,头结点取出。头指针总是指向队列中的第一个节点(要判断是否有头结点),尾结点指向双向节点的最后一个节点。
先来了解一下state各个状态的含义;
-
CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
-
SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
-
CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
-
PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
0:新结点入队时的默认状态。
1. 唤醒子节点线程
当前节点释放,一定要释放器后继节点
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
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);
}
从队列的尾节点向前查找,查找到当前节点的下一个节点为止。这里有一个问题,为什么要从后往前查询而不是从前往后查询呢?
查找到该节点,就将该节点唤醒。
2. 排他的获取锁
这个方法很关键,当前节点自旋的获取锁,能有资格自旋获取锁的线程一定是当前头结点的下一个节点,其它节点都没有资格。
当前线程获取锁,就将该线程设置为头结点,然后退出
但是也有没有资格的线程自旋的获取锁,处理逻辑在shouldParkAfterFailedAcquire(p, node)中。
逻辑为:如果当前节点的前驱节点的状态为SIGNAL,表示该节点为没有资格获取锁,他的前驱节点都不是头结点,他肯定没有资格。
如果没有找到合适的前驱,当前线程就不能被挂起。在等待过程中,该线程被中断,就将该线程中断标志位改成true,也就是标识,已经被中断了。一开始我还没动shouldParkAfterFailedAcquire方法中的逻辑,其实就是在判断该线程是否正在等待,是否等待是通过前驱节点来判断的,如果前驱节点的状态是SIGNAL,那该节点一定处于等待中,就直接返回true了,表示处于等待中。parkAndCheckInterrupt()方法就是判断该线程是否被中断过,如果是,再加上前面的处于等待,就将该线程的中断标志位表示为true。(在parkAndCheckInterrupt中调用了isInterrupted(),判断了中断位是否为true,但是也把中断位重置了,所以才要在外层显示的将中断位设置为true)
如果前驱节点的状态>0,为CANCELLED状态 ,说明前驱节点已经被取消调度了,也就是不能在该队列中混了,这样就不能作为当前节点的前驱节点,也就是要跳过这些废的节点,重新找前驱节点。当找到合适前驱节点,更改前驱节点状态为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)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// 如果获取失败,应该讲该进程挂起
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
3. 其它
该类中还有其它方法,但是都是很容易理解。
因为AQS知识独占模式和共享模式,所以获取锁和释放锁都是有这两个版本的。
我个人觉得尝试获取锁是AQS的精华,要理解透彻。在学习之前,一定要牢牢把握一点,AQS是一个状态位,一个FIFO的双向队列。标志位来判断当前线程的状态,更具不同的状态有不同的操作,双向队列就是保持所有要竞争锁的线程,通过Node节点来封装。
还有解释一点,只有头结点的下一个节点有获取锁的资格(这也是队列的特点,先进先出原则),其它节点是没有的。还要定期清理队列,因为有的线程,慢慢的就退出了竞争锁的行列,不玩了,那肯定要把这些节点清理掉,这个操作就是在自旋获取锁这个方法中执行的。
4. 关于CAS底层实现
AQS的主要实现是用CAS,CAS是很多锁基础,是cynchronized基础,但是查看CAS源码发现,很快就调用了JVM方法,这里就看看JVM方法。
其实最终的本质就是调用了一条汇编指令cmpxchg,lock cmpxchg 后者在多处理器使用
下面就是CAS的核心JVM源码了,不在多说
// 支持CAS的操作,
jbyte Atomic::cmpxchg(jbyte exchange_value, volatile jbyte* dest, jbyte compare_value) {
assert(sizeof(jbyte) == 1, "assumption.");
// 目标属性地址,也就是在对象中的偏移地址
uintptr_t dest_addr = (uintptr_t)dest;
uintptr_t offset = dest_addr % sizeof(jint);
// 获取对象当前值地址,这是一个指针
volatile jint* dest_int = (volatile jint*)(dest_addr - offset);
// 对象当前值
jint cur = *dest_int;
// 当前值得地址
jbyte* cur_as_bytes = (jbyte*)(&cur);
jint new_val = cur;
// new_val地址
jbyte* new_val_as_bytes = (jbyte*)(&new_val);
// new_val存exchange_value,后面修改则直接从new_val中取值
new_val_as_bytes[offset] = exchange_value;
// cas关键流程,如果期待值和当前值相同
while (cur_as_bytes[offset] == compare_value) {
// 调用汇编指令cmpxchg执行CAS操作,期望值为cur,更新值为new_val
jint res = cmpxchg(new_val, dest_int, cur);
if (res == cur) break;
cur = res;
new_val = cur;
new_val_as_bytes[offset] = exchange_value;
}
// 返回当前值
return cur_as_bytes[offset];
}
疑惑点
- Node类中waitState和state有什么区别