简介
AbstractQueuedSynchronizer 就是我们常说的AQS-同步器。常用的有ReentrantLock、ReadWriteLock、CountDownLatch,内部实现都依赖AQS类,可以说AQS是实现同比必备良药。
源码分析
AQS的主要参数 state字段 表示同步的状态,需要通过传入值与state进行比较是否一致。CountDownLatch就是通过state来控制达到阻塞目的,当为0的时候,释放。
//等待队列的头节点
private transient volatile Node head;
//等待队列的尾节点
private transient volatile Node tail;
//同步状态
private volatile int state;
AQS内部通过FIFO队列实现排队获取资源,通过Node的waitStatus来控制节点的状态,再由状态判断资源是否应该被该线程获取,
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
//取消状态 被打断或者超时,相当于该节点被废了
static final int CANCELLED = 1;
//等待触发状态 当unpart时,只有被unpark才能释放
static final int SIGNAL = -1;
//等待条件状态 等待某条件触发
static final int CONDITION = -2;
//状态需要向后传播
static final int PROPAGATE = -3;
//节点等待状态
volatile int waitStatus;
//上一个节点
volatile Node prev;
//下一个节点
volatile Node next;
//节点对应的线程
volatile Thread thread;
.....
}
获取锁资源
acquire方法是AQS提供对外占用资源的接口
1 首先调用子类重写tryAcquire方法,资源已被占用,则添加节点入队
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
入队主要通过自旋的方式、cas安全的将节点放至队列中
private Node enq(final Node node) {
//自旋
for (;;) {
Node t = tail;
//尾节点为空,说明队列是空的 则赋值头部、尾部都为该节点
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
//将上一个节点设置为当前尾节点
node.prev = t;
//cas设置尾节点为改node 并且在设置原为节点的next指向当前node
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
//添加等待节点
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
//将当前节点的上一个节点设置为当前的尾节点
node.prev = pred;
//如果这一步成功 则就直接返回
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//上一步不成功,将调用enq方法,自旋直至设置成功
enq(node);
return node;
}
对当前节点进行自旋判断
1 当前节点的perv是否为头节点,如果是 这说明当前节点时排第一个的
2 再次尝试对比state是否满足当前值
1,2条件都满足,则返回
否则 往下走
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);
}
}
这一步主要是对等待状态进行判断
1 SIGNAL 时 直接返回true 则挂起等待被唤醒
2 >0 表示取消 则删除该节点
3 修改pred值为Node.SIGNAL 那么因为是自旋的原因,下一次有可能就是执行1步骤进行挂起操作
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;
}
获取锁的过程就这样了。最后还需要说明的是,当被唤醒的时候,还要重新再次去竞争,然后循环上面的步骤,因为AQS获取锁是非公平的
释放锁
tryReleaseShared需要子类重写
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
1 如果head=tail 说明 队列空了,直接break
2 如果头节点的waitStatus为Node.SIGNAL 说明等待被唤醒
cas修改节点的waitStatus 成功 则 唤醒等待队列
3 继续自旋
private void doReleaseShared() {
for (;;) {
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;
}
}
unparkSuccessor 才是正真唤醒释放的地方
1 由于唤醒完之后,该节点将被释放,所以要保证改节点下的下一个节点时有意义的 不能为null或者被取消
2 LockSupport.unpark(s.thread); 唤醒线程
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);
}
总结:AQS 使用cas进行安全竞争资源,如果未竞争到 则根据条件通过LockSupport.park对线程进行挂起。
如果某个线程释放资源时,则同理 通过 LockSupport.unpark唤醒线程加入竞争的行列。
各线程如果加入了node队列中,则通过FIFO公平竞争,但是毕竟是高并发的情况下,还没有加入node队列中的N个线程和node队列的第一个线程就会相互竞争资源,也就是说 获取锁是不公平的。