Java中的大部分同步类都是基于AQS实现的。AQS是一种基于模板方法模式的线程同步框架,提供了独占式EXCLUSIVE和共享SHARED两种模式的同步模板方法。
AQS的核心思想是如果被请求资源是空闲状态,那么就将当前请求资源的线程设为有效的工作线程,将共享资源设为锁定状态。否则如果共享资源被占用,就需要一套阻塞唤醒线程的机制来保证锁的分配。这个机制主要依靠CLH队列实现,那些获取锁失败的线程会封装成节点加入该队列。
AQS使用一个volatile修饰的int类型的变量state表示同步状态,通过FIFO的双向队列(即CLH队列)完成资源获取排队操作,通过CAS完成对state值的修改。具体如下图所示:
①Node节点
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
//当前节点在队列中的状态
volatile int waitStatus;
//前驱指针
volatile Node prev;
//后继指针
volatile Node next;
//当前节点的线程
volatile Thread thread;
//指向下一个处于CONDITION状态的节点
Node nextWaiter;
/**
* 共享模式返回true
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* Returns previous node, or throws NullPointerException if null.
* Use when predecessor cannot be null. The null check could
* be elided, but is present to help the VM.
*
* 返回前驱节点
*/
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节点,先来看下Node这个数据结构。
属性值和方法 | 含义 |
---|---|
waitStatus | 当前节点在队列中的状态 |
prev | 前驱指针 |
next | 后继指针 |
thread | 当前节点的线程 |
nextWaiter | 指向下一个处于CONDITION状态的节点 |
predecessor | 返回前驱节点,没有的话抛出npe |
waitStatus有几个枚举值,具体含义在源码中注释写的很明白了,这里简单记录一下
waitStatus | 含义 |
---|---|
CANCELLED,1 | 表示取消线程获取锁的请求 |
SIGNAL,-1 | 表示线程已经准备好,就等资源释放 |
CONDITION,-2 | 表示节点等待在等待队列中,节点线程等待唤醒 |
PROPAGATE,-3 | 共享模式下使用,表示需要向下传递 |
INITIAL,0 | Node初始化时的默认值 |
②同步状态State
private volatile int state;
在AQS源码中,操作state变量的方法都是final修饰的,说明自类无法重写它们。而且对state值的修改都是原子操作,通过CAS实现。
我们先看一下ReentrantLock中非公平锁的加锁代码实现,即AQS中独占式锁的实现相关
static final class NonfairSync extends Sync {
...
final void lock() {
//如果加锁时CAS修改同步状态成功,即获取锁成功,就将当前线程设置为独占线程
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//获取锁失败,进入acquire方法进行后续操作
acquire(1);
}
...
}
acquire
方法点进去是在AQS类中,代码如下
public final void acquire(int arg) {
//通过tryAcquire方法尝试获取锁,如果获取成功就不再向后执行,否则,就要将当前线程加入等待队列
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire
方法在AQS中只是定义了一个模板方法,具体实现是在子类中实现的
// java.util.concurrent.locks.AbstractQueuedSynchronizer
//AQS中protected修饰的方法都在子类中有对应的实现
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
例如在ReentrantLock的独占锁实现如下
protected final boolean tryAcquire(int acquires) {
//重写tryAcquire方法,调用自己的nonfairTryAcquire方法
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取同步状态(共享资源)
int c = getState();
//如果共享资源没有被占用
if (c == 0) {
//CAS占锁
if (compareAndSetState(0, acquires)) {
//占锁成功,将线程设置为独占线程,返回true
setExclusiveOwnerThread(current);
return true;
}
}
//当前线程是占锁线程
else if (current == getExclusiveOwnerThread()) {
//重复上锁
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//设置新的同步状态值,返回true
setState(nextc);
return true;
}
return false;
}
上面说过,如果尝试获取资源失败,会进入acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
尝试线程节点获取锁方法,下面先详细说一下addWaiter
入队方法,只有先将线程封装成节点入队之后才有可能去竞争释放的资源。
private Node addWaiter(Node mode) {
//将当前线程和锁模式(独占/共享)封装成一个Node节点
Node node = new Node(Thread.currentThread(), mode);
//pred指针指向CLH队列尾节点
Node pred = tail;
//如果pred不为null,说明队列中有节点
if (pred != null) {
//将新节点的前驱指针指向pred
node.prev = pred;
//通过CAS操作完成尾节点的设置(即将当前节点设为尾节点)
//这个方法主要是对tailOffset和Expect进行比较,如果tailOffset的Node和Expect的Node地址是相同的,那么设置Tail的值为Update的值
//private final boolean compareAndSetTail(Node expect, Node update)
//return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果pred是null,说明队列中没有节点或者当前pred指针和tail指向的位置不同(说明被别的线程修改了)那么就要看一下enq方法的实现
enq(node);
return node;
}
//enq方法
private Node enq(final Node node) {
//一个无限循环(即自旋)
for (;;) {
//获取tail节点的指针
Node t = tail;
//如果是null,就需要进行初始化,下面的原注释如此
if (t == null) { // Must initialize
//初始化一个头节点,注意这个头节点不是当前线程节点,而是调用Node的一个无参构造方法的节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
//如果是尾节点被其他线程修改,那么获取新的尾节点之后再次使用CAS将当前线程节点入队设为尾节点
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
综上,可以看出,addWaiter()
方法就是将当前线程节点入队并设为尾节点的操作,需要注意的是,这个队列的头节点是一个无参构造的头节点。这个入队流程如下图所示:
在addWaiter()
方法执行完成后返回了包含当前线程的节点,而这个节点会作为参数,进入acquireQueued
方法,该方法会对队列中的线程进行获取锁的操作。总的来说,一个线程获取锁失败,就被放入等待队列,acquireQueued
会把队列中的线程不断去获取锁,直到成功或者中断。下面从“何时出队列”和“如何出队列”两方面分析一下其源码:
final boolean acquireQueued(final Node node, int arg) {
//标志是否成功获取资源
boolean failed = true;
try {
//标记等待过程中是否中断
boolean interrupted = false;
//自旋,要么获取锁,要么中断
for (;;) {
//获取当前节点的前驱节点,这个方法在Node类中说过
final Node p = node.predecessor();
//如果p是头节点,说明当前节点在队列的前端,就可以去尝试获取资源,因为前面说过队列的头节点是个无参构造的节点,实际可以获取资源的是头节点的下一个节点
if (p == head && tryAcquire(arg)) {
//获取资源成功,将当前节点设为头节点,清除thread属性和前驱节点属性
setHead(node);
//将p节点置为null,方便GC回收
p.next = null; // help GC
failed = false;
return interrupted;
}
//`shouldParkAfterFailedAcquire`百度翻译是失败后是否应停止获取,所以下面这段代码意思明了了,即p是头节点且当前节点线程没有获取到锁或者p压根就不是头节点,这个时候就要判断当前节点是否要被阻塞,防止无限循环浪费资源
//注意这里`shouldParkAfterFailedAcquire`返回true的情况只能是前驱节点处于signal状态,这样才可能阻塞当前线程;否则,当前线程继续自旋尝试获取锁
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//最终获取资源失败
if (failed)
//取消获取资源
cancelAcquire(node);
}
}
//shouldParkAfterFailedAcquire,通过前驱节点判断当前线程是否该被阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//前驱节点的状态值
int ws = pred.waitStatus;
//如果前驱节点处于SIGNAL(唤醒)状态,只等资源释放
if (ws == Node.SIGNAL)
//那么当前节点可以阻塞,返回true继续`parkAndCheckInterrupt()`执行
return true;
//ws>0代表取消状态
if (ws > 0) {
//前驱节点已经因为超时或响应了中断取消了当前线程节点,所以需要跨越掉这些CANCELLED节点,直到找到一个<=0的节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//通过CAS设置前驱节点的等待状态为signal
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
//返回false,当前线程继续自旋尝试获取锁
return false;
}
//`parkAndCheckInterrupt`主要用于挂起当前线程,阻塞调用栈,返回当前线程的中断状态。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
出队流程如下图所示:
在前面有判断前驱节点状态为等待状态的情况,那么取消节点是怎么生成的?又是在什么时间释放节点通知到被挂起的线程呢?
在acquireQueued
方法的finally块中,有个cancelAcquire
方法,将Node的状态标记为CANCELLED。代码如下:
private void cancelAcquire(Node node) {
// 将无效的节点过滤
if (node == null)
return;
//将节点的线程设为null,当前节点成为虚节点
node.thread = null;
//获取当前节点的前驱节点
Node pred = node.prev;
//通过前驱节点,跳过取消状态的节点,将找到的节点作为前驱节点
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
//获取过滤后的前驱节点的后继节点
Node predNext = pred.next;
//将当前节点设置为取消状态
node.waitStatus = Node.CANCELLED;
//如果当前节点是尾节点,将上面获取的当前节点的前驱节点设为尾节点
if (node == tail && compareAndSetTail(node, pred)) {
//更新成功,将尾节点的后继节点设为null
compareAndSetNext(pred, predNext, null);
} else {
//更新尾节点失败
int ws;
//如果前驱节点不是头节点
//1、前驱节点是signal
//2、前驱节点不是signal和cancel,并且设置为signal成功
//1或2满足一个条件,并且前驱节点的线程不是null
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
//将前驱节点的后继节点设为当前节点的后继节点
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
//当前节点是头节点的后继节点或上述条件不成立,唤醒当前节点
unparkSuccessor(node);
}
//将当前节点出队
node.next = node; // help GC
}
}
在上述代码中,一直是在对Next指针做修改,而没有对Perv指针操作,这是为什么?何时会对Prev指针进行操作?
执行cancelAcquire()
方法时,可能prev在acquireQueued
方法的try代码块中已经出队了,如果此时修改prev指针,可能会导致指向移除队列的节点,不安全。
shouldParkAfterFailedAcquire
方法中,执行了node.prev = pred = pred.prev;
,这行代码是在获取锁失败的情况下执行的,这时共享资源已被占用,当前节点之前的节点都不会再变化,这时候变更prev更安全。
public void unlock() {
sync.release(1);
}
可以看出,解锁并不会区分公平锁和非公平锁,最终都是通过继承AQS的sync子类调用AQS的release
方法,代码如下:
public final boolean release(int arg) {
//尝试释放锁,同尝试获取锁类似,这是个模板方法,具体实现看子类实现
//如果成功,说明资源已被释放
if (tryRelease(arg)) {
//头节点
Node h = head;
//头节点不为null且头节点不是初始化状态
//h==null代表还没有节点入队
//h.waitStatus == 0代表头节点刚刚初始化
if (h != null && h.waitStatus != 0)
//解除线程挂起状态,该线程可以去获取资源
unparkSuccessor(h);
return true;
}
//尝试释放锁失败,释放锁失败
return false;
}
//ReentrantLock中尝试释放锁的具体实现
protected final boolean tryRelease(int releases) {
//释放后占用次数
int c = getState() - releases;
//当前线程不是资源占用线程
if (Thread.currentThread() != getExclusiveOwnerThread())
//抛出异常
throw new IllegalMonitorStateException();
//资源释放标志
boolean free = false;
if (c == 0) {
//如果资源锁定次数为0,即已被释放,标志置为释放
free = true;
//将资源占用线程设为null
setExclusiveOwnerThread(null);
}
//修改state值
setState(c);
//返回是否资源释放
return free;
}
//看一下唤醒线程的方法
private void unparkSuccessor(Node node) {
//头节点的等待状态
int ws = node.waitStatus;
//小于0,将头节点设为初始化状态
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//获取头节点的后继节点
Node s = node.next;
//如果下个节点是null或下个节点处于取消状态,找队列最开始的非canceled状态节点
if (s == null || s.waitStatus > 0) {
s = null;
//从尾节点向前寻找队列中第一个waitStatus<=0的节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//如果头节点的下个节点不为null,就把头节点的后继节点唤醒
if (s != null)
LockSupport.unpark(s.thread);
}
上面就是释放锁的整个流程了,相对于加锁,释放锁就显得简单明了了。总结一下就是,线程释放锁,由于是可重入锁,所以只有将资源的所有重入次数清零,资源才算是真正释放,这时,AQS就会唤醒等待队列的“头节点”(注意这里加了引号,因为不是真正的头节点,而是等待队列中所有有效的等待节点的第一个)去获取资源。
除了ReentrantLock的可重入性应用,AQS作为并发编程框架,还向其他同步工具提供了应用。
同步工具 | 同步工具与AQS的关联 |
---|---|
Semaphore | 使用AQS同步状态来保存信号量的当前计数。tryRelease会增加计数,acquireShared会减少计数。 |
CountDownLatch | 使用AQS同步状态来表示计数。计数为0时,所有的Acquire操作(CountDownLatch的await方法)才可以通过。 |
ReentrantReadWriteLock | 使用AQS同步状态中的16位保存写锁持有的次数,剩下的16位用于保存读锁的持有次数。 |
当然,我们也可以自定义自己的同步工具,去实现一些功能。如下,是一个简单实现:
//自定义的锁
public class AqsDemo {
private static class Sync extends AbstractQueuedSynchronizer{
//重写尝试获取锁
@Override
protected boolean tryAcquire(int arg){
return compareAndSetState(0,1);
}
//重写尝试释放锁
@Override
protected boolean tryRelease(int arg){
setState(0);
return true;
}
//是否持有资源
@Override
protected boolean isHeldExclusively(){
return getState() == 1;
}
}
private Sync sync = new Sync();
public void lock(){
//调用aqs的占资源方法
sync.acquire(1);
}
public void unlock(){
//调用aqs的释放资源方法
sync.release(1);
}
}
测试类,无论运行多少次,count都是2000,当然,这里我用sleep阻塞主线程,让A和B线程执行,其实这不是一个好的方法,仅作为demo测试使用。
public class TestAqsDemo {
static int count = 0;
final static AqsDemo aqsDemo = new AqsDemo();
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
aqsDemo.lock();
System.out.println(Thread.currentThread().getName()+"运行");
for(int i=0;i<1000;i++){
count++;
}
aqsDemo.unlock();
},"A").start();
new Thread(()->{
aqsDemo.lock();
System.out.println(Thread.currentThread().getName()+"运行");
for(int i=0;i<1000;i++){
count++;
}
aqsDemo.unlock();
},"B").start();
Thread.sleep(5000);
System.out.println(count);
}
}
这篇只针对AQS的独占模型的部分代码进行了分析,而共享式也是很重要的一块内容,需要再去阅读一下代码,丰富一下自己的知识。我觉得文中的几个流程图对于理解代码有很好的帮助,可以考虑自己跟着流程图去阅读源码。最后,再次感谢开源作者的分享。