AQS概述
AbstractQueuedSynchronizer(AQS)抽象队列同步器,是Doug Lea实现的一个用于同步多线程的一个组件。java.util.concurrent包下的一些Lock实现类就是基于AQS实现的,如常见的ReentrantLock、ReentrantReadWriteLock等。
AQS内部实现了一个双向队列的数据结构,用于存储在线程请求锁时,需要阻塞的线程。
锁可分为独占锁和共享锁;
独占锁也叫排他锁,就是锁只能被一个线程所持有,如果一个线程持有了独占锁,那么其他线程请求时,将被阻塞,知道持有锁的线程释放锁;
共享锁可以被多个线程持有,共享锁被持有时,可继续被其他请求该锁的线程加锁,如果有一个线程获取到该数据的独占锁,那么其他线程只能对数据加共享锁,不能加排他锁;获取到排他锁的数据可以读写数据,而获取到共享锁的线程只能读数据。
下面将结合AQS、ReentrantLock、ReentrantReadWriteLock 源码理解AQS具体实现方式。
AQS主要结构
AQS中,队列的实现是通过内部类Node实现的,同时还有队首、队尾等成员变量:
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
implements java.io.Serializable {
//队列懒加载的,如果锁第一次被线程请求时哦,不会初始化head和tail,没有队列,只有出现多个线程竞争
//同一个锁时,才创建队列
private transient volatile Node head;//队首节点,实际不存任何线程,下一个才是等待队列的第一个元素
private transient volatile Node tail;//队尾节点
private volatile int state;//当前锁被持有的线程个数
//队列实现类
static final class Node {
static final Node SHARED = new Node();//锁类型,共享锁
static final Node EXCLUSIVE = null;//锁类型,独占式锁
static final int CANCELLED = 1;//状态,表示线程被取消
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;//当前节点对应的线程
Node nextWaiter;//SHARED 时表示是共享锁,否则链接到Condition队列中的下一个节点
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;
}
}
//请求共享锁,由子类实现,返回大于0表示加锁成功
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
//请求独占锁,由子类实现,返回true表示加锁成功
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
//释放独占锁,由子类实现,返回true表示当前锁无线程占用,抛出异常表示释放失败
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
//释放共享锁,由子类实现,返回true表示当前锁无线程占用,抛出异常表示释放失败
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
...
需要注意的是,AQS中有四个方法,需要由子类实现,JUC中的一些Lock类就是通过不同的实现方式,实现了可重入锁、共享锁,独占锁等。
具体的这四个方法在不同锁中的实现细节,可以看我另外一篇文章 Java中的锁介绍
AQS中,使用了CAS算法设置变量值,CAS方法可以看成是原子操作,是线程安全的。
独占锁加锁
下面通过ReentrantLock来展开。ReentrantLock是一个可重入锁的独占式锁,可以通过构造方法初始化为公平锁或非公平锁的实现方式,默认是非公平锁。使用方式:
public class LockTest {
static ReentrantLock lock = new ReentrantLock();
public void method(){
lock.lock();
//doSomething ...
lock.unlock();
}
}
中间的doSomething就是需要同步的代码块,看lock()方法源码,调用的是ReentrantLock内部类Sync的lock()方法,而Sync类继承了AQS,并且有NonfairSync和FairSync两个实现方式,这里我们用非公平锁请求锁过程来展开。
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {...}
public void lock() {
sync.lock();
}
static final class FairSync extends Sync {...}
static final class NonfairSync extends Sync {
final void lock() {
//CAS算法加锁,原子性操作
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());//加锁成功,设置锁的独占线程为当前线程
else
acquire(1);//锁被其他线程占有,请求锁操作
}
//获取锁的实现方法
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
...
AQS中,有一个int类型的state成员变量,用于存储锁被线程请求的次数,为0时表示无线程占有该锁。compareAndSetState(CAS)尝试加锁,既设置state为0,设置失败时,说明有其他线程已经持有该锁,此时调用acquire(1)方法,继续请求锁,请求数量为1.
acquire方法是AQS中的方法:
public final void acquire(int arg) {
//如果请求锁失败,并且当前线程成功加入队列中时
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();//阻塞当前线程
}
acquire方法调用的是tryAcquire方法来判断是否成功加锁,该方法在NonfairSync类中,调用了Sync类中的nonfairTryAcquire方法:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();//当前线程
int c = getState();//获取持有锁的线程个数
if (c == 0) {//没有锁持有线程时
if (compareAndSetState(0, acquires)) {//通过CAS加锁
setExclusiveOwnerThread(current);//设置当前线程为锁的独占式持有线程
return true;
}
}
//如果c>0,说明有其他锁持有该线程,如果持有锁的线程也是当前线程,那么发生了重入的情况,可以获取锁
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;//获取锁的数量加1
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//否则获取失败
return false;
}
可以看到,ReentrantLock是可重入锁。如果获取锁失败时,将调用acquireQueued方法,并且在调用acquireQueued方法前,先调用了addWaiter方法把当前线程加入队列中,addWaiter方法:
//参数mode是Node类的SHARED或EXCLUSIVE,表明该线程想获取的是独占式锁还是共享锁
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 这里是快速将节点插入队列的操作
Node pred = tail;
if (pred != null) {//队尾不为空,说明队列中有等待的线程
node.prev = pred;
// CAS方式插入队列尾部,如果失败,则可能出现有其他线程也请求锁,并已经进入队列中
// 或者尾部的那个线程已经释放锁,不在队列中了
if (compareAndSetTail(pred, node)) {
pred.next = node;//成功插入队列尾部
return node;
}
}
//如果队尾为空,或者出现插入队列尾部失败时,调用enq方法插入队列
enq(node);
return node;//返回新创建的节点
}
快速插入队尾失败时,可能是队列为空,或者出现其他线程插入了队列尾部,这时调用enq方法中,通过自旋的方式不断尝试将该线程插入队尾中;并且如果队列为空时,将初始化队列:
//node为需要插入队列的节点
private Node enq(final Node node) {
for (;;) {//自旋
Node t = tail;
// 如果队尾为空,说明需要初始化队列,head节点new Node()即可,不存储线程
// 同时继续循环,设置队尾
if (t == null) {
if (compareAndSetHead(new Node()))//CAS设置队首
tail = head;
} else {//同addWaiter方法中一样,插入队尾
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
总的来说,addWaiter方法就是将当前线程先快速插入到等待队列的尾部,如果队列为空时,或者插入队列尾部失败,则以自旋的方式继续尝试插入队列,直到成功。
addWaiter方法执行完后,此时当前请求锁的线程已经被插入队列尾部了,并返回当前线程的Node节点,但当前线程还没有阻塞。
在acquire方法中,我们发现这个节点被传入acquireQueued方法了。
acquireQueued方法主要用于阻塞当前线程,并保证在阻塞后被重新唤醒时能够继续去执行请求锁的操作。
acquireQueued方法如下:
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; // 释放上一个队首的在队列中的链接,帮助JVM回收
failed = false;
return interrupted;//返回线程是否阻塞过
}
// 如果获取锁失败,则判断是否在获取锁失败时需要阻塞线程
// 如果需要阻塞线程,调用parkAndCheckInterrupt方法阻塞线程,线程在这里暂停运行
// 重新被唤醒时,将继续循环,请求锁
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 如果线程一次也没有获取到锁,取消该线程
if (failed)
cancelAcquire(node);
}
}
总的来说,acquireQueued就是将当前线程放在一个循环中,如果当前线程是队列中第一个线程,则请求锁,请求不到时如果需要阻塞,则阻塞当前线程;重新被唤醒时继续在循环中请求锁。
shouldParkAfterFailedAcquire方法用于判断在请求锁失败的情况下,是否可以阻塞当前线程,当前面一个节点的状态为SIGNAL时,才表明自己有机会被唤醒,这时才能阻塞当前线程:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
//如果前一个线程的状态是SIGNAL,那么说明当前线程有机会被重新唤醒,此时可以阻塞
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中的release方法:
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
//释放锁
if (tryRelease(arg)) {
Node h = head;
//成功释放锁后,需要唤醒下一个线程
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;//成功释放
}
return false;//释放失败
}
tryRelease方法在Sync类中实现,公平和非公平锁都是调用同一个释放锁的方法:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;//释放锁后,持有该锁的线程数
//如果当前线程没有持有该锁,抛出异常,释放失败
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;//是否有线程持有锁
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);//独占锁释放时,只会是当前线程释放锁,不需要CAS,直接释放即可
return free;
}
释放锁仅需将state设置为释放后的state即可。
再看unparkSuccessor方法,该方法是用于唤醒队列中等待锁资源的线程里,位于队伍最前面的线程,唤醒后的线程将继续执行acquireQueued中的代码尝试获取锁:
// 唤醒node节点后面的线程
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// 如果node节点状态不是取消状态,将状态设为0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;//下一个节点
// 如果下一个节点为空,或者是取消状态
// 此时尝试从队列尾部往前,找离node节点最近的一个等待节点
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);//唤醒线程
}
共享锁加锁
ReentrantReadWriteLock类也叫读写锁,其内部有两把锁:读锁和写锁,读锁是共享锁,写锁是排他锁,分别由内部类ReadLock和WriteLock实现,同时还有一个内部类Sync,继承了AQS:
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** Performs all synchronization mechanics */
final Sync sync;
WriteLock是独占锁,所以调用的是acquire请求加独占锁
public static class WriteLock implements Lock, java.io.Serializable {
private final Sync sync;
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
public void lock() {
sync.acquire(1);//请求加独占锁
}
而ReadLock请求加的是共享锁,调用的是AQS中的acquireShared方法:
public static class ReadLock implements Lock, java.io.Serializable {
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
public void lock() {
sync.acquireShared(1);//请求加共享锁
}
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
acquireShared方法调用tryAcquireShared方法请求获取共享锁,该方法由子类实现。
如果返回值小于0,则表明获取共享锁失败,此时将调用doAcquireShared方法;如果等于0,则表示获取当前共享锁成功,并且共享锁数量已经用完,后续线程不能再获取锁;如果大于0,则表示当前线程获取锁成功,并且还有剩余的锁资源可被其他线程获取。
doAcquireShared和acquireQueued方法类似,也是将当前线程加入队列,并判断是否需要阻塞当前线程:
private void doAcquireShared(int arg) {
//这里的mode是SHARED,表明加入队列的是请求共享锁类型的线程
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
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);
}
}
不同的地方在于当请求到锁时,独占锁调用了setHead方法,而共享锁调用了setHeadAndPropagate方法:
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
// 这里的propagate参数一定大于等于0
// 等于0时表示本次获取锁成功后,共享锁资源已经被用完
// 大于0说明还有锁资源可被其他线程使用
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
// 这里的判断是如果还有可用资源
// 或者无可用资源,但是此时又有其他线程释放了锁
// 可以尝试唤醒队列中的线程去请求锁
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
如果propagate大于0,说明本次请求完后,还有共享锁资源可使用,此时可尝试去唤醒队列等待中线程去请求锁,唤醒操作在doReleaseShared方法中。
如果不满足propagate大于0,即等于0,无共享锁资源可使用,需要注意的是,由于当前是多线程环境,所以propagate等于0,并不代表无共享锁资源可用,也可能出现在当前线程运行到if判断时,有其他线程正好释放了锁。
这里,h是旧表头的引用,h == null的判断仅为了避免空指针错误,因为在此之前已经调用了addWaiter方法,所以不会出现h==null的情况,而 (h = head) ==null时,h引用已经变成新队首节点的引用了。
如果h.waitStatus < 0,此时就可能出现其他线程在当前线程运行到这里时,释放了锁,至于为什么,需要看doReleaseShared中的逻辑,这个方法是释放共享锁时的逻辑。
private void doReleaseShared() {
for (;;) {
Node h = head;
// 如果队列中有等待的线程
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
//将队首状态设为0
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);// 锁成功释放,并唤醒等待线程
}
// 如果ws ==0 则说明有其他线程在执行上一个if操作
// 此时将状态设为PROPAGATE,以便被其他线程检测到
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
可以看到,在doReleaseShared方法中,有一步是将head的状态设置为PROPAGATE的,这个值也是负数,说明有其他线程在释放锁。
再回到setHeadAndPropagate方法中的if判断,第一个h.waitStatus < 0表明有其他线程在释放锁;第二个h.waitStatus < 0,此时的h已经是新队首元素,此时h.waitStatus < 0小于0很正常,因为h可能就是刚刚的node,waitStatus值是SIGNAL。
if (s == null || s.isShared()) 表明,如果队列中下一个线程即不是空,并且是独占模式,那不需要唤醒线程,否则再执行doReleaseShared方法,判断是否需要唤醒线程
setHeadAndPropagate中的if判断这样写也是为了减少唤醒不必要的线程,避免唤醒后的线程获取不到锁时再次阻塞
总的来说,共享锁在获取锁时,如果当前线程获取锁后,还有锁资源可被其他线程获取,那么将尝试唤醒其他线程。
共享锁释放
释放锁的代码也很简单,就是调用子类实现的tryReleaseShared方法,如果成功释放,则调用doReleaseShared方法去处理后续的唤醒线程操作
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
本文是本人的学习备忘和知识积累,不足或理解错误之处请包涵和指正