AQS简介
在一个JVM中采取同步的方式:
- 显示锁:ReentrantLock
- 内置锁:Synchronized
- 原子类:如AtomicInteger、AtomicLong、AtomicBoolean等
AbstractQueuedSynchronizer(AQS)是一个用于构建锁和同步器的框架。基于(AQS)构建的有ReentrantLock、CountDownLatch、Semaphore、FutureTask等。其中ReentrantLock是独占锁、CountDownLatch、Semaphore是共享锁。CyclicBarrier、阻塞队列是基于ReentrantLock来实现的。
AQS利用CSA来实现,即利用cpu的原子指令comparAndSet来实现。
基于AQS可以构建共享锁、独占锁。本文以独占锁为例。
AQS中的属性和要实现的方法
以上变量均通过CAS进行变量赋值(保证赋值时的原子操作)且都是volatile变量(值更改后线程可见)。变量赋值类似,如state赋值:
protected final boolean compareAndSetState(int expect, int update) {
// 当前state值与expect值相同,则把update的值赋给state
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
要点:state变量,锁标志状态(线程通过或者线程阻塞)。在ReentrantLock中是重入锁的个数。CountDownLatch、Semaphore中是共享锁的个数。
AQS供子类实现的方法如下:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
//上述两个方法构建独占锁同步器需要实现
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
//上述两个方法构建共享锁锁同步器需要实现
protected boolean isHeldExclusively() {//该线程是否正在独占资源。只有用到condition才需要去实现它。
throw new UnsupportedOperationException();
}
问题提出:
①AQS如何把没有获取到锁的线程挂起,放到阻塞队列中?
②AQS什么条件唤醒挂起的线程?
③唤醒线程时,为什么从尾节点遍历?
AQS中的线程阻塞
以ReentrantLock中的非公平锁为例:请打开它的源码,探得其中机理。第一次获取锁成功,此时不会产生阻塞队列,获得锁之后程序继续执行,假设线程名称为successThread。当successThread线程没有释放锁即程序没执行完,此时有其它线程获取lock锁,其它线程就会阻塞,阻塞的线程会放到阻塞队列中。阻塞队列就是双向链表(AQS中的node)。
final void lock() {
//原子操作执行成功代表获取锁成功。成功的条件必须当前的state值为0
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//获取失败执行AQS中的acquire方法
acquire(1);
}
以AQS中的acquire方法作为切入点:
public final void acquire(int arg) {
//tryAcquire需要自己实现,重点解析下面的几个方法
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
调用tryAcquire时序图:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//设置可重入锁的数量,当前线程必须与lock方法设置的线程相等
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
其它线程获取锁时会直接返回false。
接着分析addWaiter方法。node数据结构参考LinkedList中node的数据结构。是双向链表。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 第一个没有获取到锁的线程,此时tail必为null
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {//尾节点已指向node,不需要再把尾节点重新指向node
pred.next = node;
return node;
}
}
enq(node);
return node;
}
enq方法。在第一次插入node或设置尾节点失败,执行此方法。
作用:自旋直至把Node放入到阻塞队列中。
private Node enq(final Node node) {//node的
for (;;) {//自旋直至把Node放入到阻塞队列中
Node t = tail;
if (t == null) {
if (compareAndSetHead(new Node()))//初始化头节点
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {//尾节点指向node
t.next = node;
return t;
}
}
}
若tail为null第一次遍历初始化头节点,new了一个node对象其中prev,next,thread均为null。第二次遍历才把参数中的node放到头节点的后面。
acquireQueued方法。
作用:自旋直至获取锁或者进行线程阻塞,若线程中断则把waitStatus设置成Node.CANCELLED,值为1。
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; //之前的head节点不存在
failed = false;
return interrupted;
}
//shouldParkAfterFailedAcquire与parkAndCheckInterrupt返回false则会继续循环
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())//线程阻塞
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire方法。
作用:找到阻塞队列中没有线程中断的线程并把WaitStatus设置成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 {//把阻塞的node设置成待唤醒状态
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
parkAndCheckInterrupt方法。
作用:阻塞线程,唤醒后返回线程中断标志。若线程被中断了则会调用selfInterrupt()方法。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//进行阻塞,this是调用acquire方法的对象,
//在本例中指的是NonfairSync对象。并不会再往下执行,需要等待被唤醒
return Thread.interrupted();//返回线程中断的标志
}
注意:
- enq方法跟acquireQueued方法的自旋结束条件。插入第一个节点时,enq会创建一个node。
- parkAndCheckInterrupt中把线程阻塞
- acquireQueued方法获取到锁之后会把原head给释放,并把原head的第一个尾节点作为head。
AQS中的线程唤醒
ReentrantLock中的unlock方法,调用的是AQS中的release方法
public final boolean release(int arg) {
if (tryRelease(arg)) {//调用ReentrantLock.Sync中的tryRelease方法
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);//唤醒线程
return true;
}
return false;
}
tryRelease方法。
作用:因为ReentrantLock是可重入的,返回true时唤醒线程。返回true的条件是state为0。
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {//为0返回的结果才是true
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
注意:比如重入锁的个数为3,前两次调用unLock时不会唤醒阻塞的线程,只会改变state的值。
unparkSuccessor方法。
作用:唤醒指定线程。
private void unparkSuccessor(Node node) {
//第一次调用传入的node为head。node.waitStatus值为0
int ws = node.waitStatus;
if (ws < 0)//在shouldParkAfterFailedAcquire方法中状态已设置为Node.SIGNAL即值为-1
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中唤醒线程从尾节点遍历的原因
从尾节点遍历的原因是从头节点遍历会出现断链的情况。
在addWaiter方法、enq方法给尾节点赋值(compareAndSetTail)之后,还需从尾节点指向当前要插入链表的节点即t.next = node;执行compareAndSetTail是原子操作,执行t.next = node;不是原子操作不能保证线程间的顺序问题。
以使用ReentrantLock为例假设:
有三个线程,线程名为A、B、C。线程A正在执行,已抢到锁。线程B、线程C同时执行addWaiter中的compareAndSetTail方法,线程B设置尾节点成功,线程C在enq方法设置尾节点成功。线程B没有执行t.next = node;线程C已执行。此时线程A释放锁,执行unparkSuccessor寻找后续节点。如图:
说明:线程A正在执行
说明:线程B已执行addWaiter中的compareAndSetTail。但还没有执行pred.next = node;
说明:线程C已执行enq中的compareAndSetTail且已执行pred.next = node;线程A释放锁之后,若从头节点进行遍历则next指向的为null。获取不到节点。无论pre还是next指向的是node的地址,node包含pre跟next。并不是pre指向next,next指向的是pre。
总结:
- 程序在运行的时候是基于线程的,线程阻塞即程序阻塞。
- 使用显示锁,调用lock方法时程序便阻塞在parkAndCheckInterrupt()方法。程序不再继续执行,等待唤醒线程,进而再执行程序。
- release方法调用tryRelease方法尝试唤醒(LockSupport.unpark),被唤醒的线程在阻塞的地方继续执行,即在acquireQueued方法的parkAndCheckInterrupt()方法继续执行。
- 唤醒线程时,是指定唤醒,唤醒的是head节点中的第一个尾节点。
- 唤醒线程时,采用的是尾节点遍历。链表有断链的问题。
本文只分析了独占锁,有理解不当之处望指出。
每篇一语
花总是要开的