java AQS源码学习
AbstractQueuedSynchronizer
AbstractQueuedSynchronizer抽象队列同步器, 简称AQS, 只有读懂了AQS的源码才能看懂ReentrantLock可重入锁是如何实现的
Node
node是AQS中队列的结点结构, 用于存放aqs队列中的线程引用
static final class Node {
// SHARED表示线程是因为获取共享资源失败被阻塞所以添加入队列
static final Node SHARED = new Node();
// EXCLUSIVE则是因为独占线程失败所以添加入队列
static final Node EXCLUSIVE = null;
// 表示线程因为中断或者等待超时, 需要从队列中取消等待
static final int CANCELLED = 1;
// 表示结点正在等待唤醒
static final int SIGNAL = -1;
// 表示结点在Condition上等待, 其他线程调用Condition的signal()方法后
// 这个状态的结点会从等待队列进入同步队列
static final int CONDITION = -2;
// 共享模式下, 前驱结点不仅会唤醒后继结点, 也会唤醒后继的其他结点
static final int PROPAGATE = -3;
// 结点的等待状态, 值为上文中的常量
volatile int waitStatus;
// 结点的前驱结点
volatile Node prev;
// 结点的后继结点, 这里可以看出结构是个双链表
volatile Node next;
// 在构造器中, 会传入mode赋值给nextWaiter, 在下面的isShared方法中
// 会判断该线程是否因为获取共享资源失败而进入的队列
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
// 获取该结点的前驱结点
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
后面的方法中, 可以反复来看Node中的结构
一些变量和方法
// AQS队列中的链表头
private transient volatile Node head;
// AQS队列中的链表尾
private transient volatile Node tail;
// 当前AQS的状态, 为0则是未上锁
private volatile int state;
// 因为state是volatile修饰的, 所以可以直接获取
protected final int getState() {
return state;
}
// 如上
protected final void setState(int newState) {
state = newState;
}
// 对volatile修饰的变量获得并修改的操作不是原子性的, 所以这里使用CAS来修改变量
// 这个方法用于获得或者释放锁, 先对比state状态, 再修改
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
addWaiter和enq
addWaiter方法构造了一个新的结点插入队列, 而enq方法保证了这个插入操作的原子性, 保证了在多线程的情况下, 队列的一致性
private Node addWaiter(Node mode) {
// 使用当前线程和mode构造一个新的node
Node node = new Node(Thread.currentThread(), mode);
// 获取当前队列的链表尾
Node pred = tail;
if (pred != null) {
// 若链表中除了头结点外还存在结点, 则将链表尾设置为node的前驱
node.prev = pred;
// 使用cas操作替换链表尾, 将新插入的结点设置为链表尾
// 如果成功就直接返回node, 如果失败就会进入enq进行一个自旋的cas操作
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
private Node enq(final Node node) {
// 这里是一个自选操作
for (;;) {
Node t = tail;
// t为空代表链表未初始化, 先进行初始化
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 这里与addWaiter的插入操作相同, 使用cas确保操作的原子性
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
acquire以及相关方法
这里是获取锁的方法
public final void acquire(int arg) {
// 尝试获取锁, 如果失败则将node放入队列尾部然后调用acquireQueued方法自旋尝试获取资源
// selfInterrupt会给该线程设置一个中断标志
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 一个自旋
for (;;) {
// 先获取该结点的前驱
final Node p = node.predecessor();
// 如果前驱是head并且尝试去获取锁成功
if (p == head && tryAcquire(arg)) {
// 将当前结点设置为头结点, 因为当前节点将要获取资源执行任务
setHead(node);
// 将之前的头结点的后驱结点设为null, 这样去除了引用关系可以等待gc
p.next = null; // help GC
failed = false;
return interrupted;
}
// 调用shouldParkAfterFailedAcquire查看是否可以进入等待
// 然后进入等待, 如果等待过程被中断过一次, 那么interrupt就是true
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 如果等待过程中被中断或者等待时间结束, 就取消该结点在队列中的等待
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取前驱结点的waitStatus, 这个状态的状态码在node中写过
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 这里如果前驱的状态码是SIGNAL说明已经告知前驱, 如果前驱执行后就通知自己
return true;
if (ws > 0) {
// 如果前驱已经放弃等待, 就往前寻找正在等待的结点
// 这些放弃等待的结点由于引用链的断开会被gc回收
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
// 找到正在等待的pred后, 将pred的后继设为node, 这里等于将中间的所有结点废弃
pred.next = node;
} else {
// 如果前驱状态正常, 则使用cas将前驱状态设置为SINGAL, 因为是多线程环境
// 这里的ws可能会被线程自己修改, 所以需要使用cas替换
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
// 使用park方法将自己进入waiting状态
// 这里的park也是通过cas操作来实现等待状态
LockSupport.park(this);
// 返回线程的中断状态
return Thread.interrupted();
}
所以总的来说acquire方法做了四件事情
- 调用tryAcquire方法去尝试获取资源
- 否则, 调用addWaiter方法将该线程放入等待队列的尾部, 并且标记为独占模式(signal)
- acquireQueued使线程在等待队列中等待, 当被unpark的时候就会被唤醒然后尝试获取资源, 如果在等待的途中被中断过, 这个方法就会返回true, 否则返回false
- 在acquire方法中, 在acquireQueued这个方法返回true的时候会进行自我中断, 保证了线程一定会被中断
release
释放锁的时候使用的就是release
在aqs中tryRelease和tryAcquire一样都是需要实现类去实现的方法
他们在aqs中只有一句throw
public final boolean release(int arg) {
// 尝试去释放资源
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
// 这里唤醒了等待队列中的下一个线程
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
// 获取当前结点的等待状态
int ws = node.waitStatus;
// 将当前结点状态置零, 这里没有循环所以是允许失败的
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
// 这里判断状态为0或者已取消则不进入语句块
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;
}
// 如果上面在for循环中找到了有效结点s, 则进行unpark唤醒
if (s != null)
// 这里unpark方法中先判断thread是否为空再使用UNSAFE去唤醒
LockSupport.unpark(s.thread);
}
释放比获取更容易, 因为释放只需要对状态进行操作, 而获取中需要对链表进行修改
后记
学习完AQS的源码之后, 会去学习一下ReentrantLock的源码, 基本上涉及到并发操作的变量都会用volatile修饰并且用cas来修改, 在ConCurrentHashMap中也是使用这一操作来对jdk1.7中的分段锁进行的优化