本文转至 链接太长 ,如果页面失效,请直接关注微信公众号 “小孩子4919 我们都是小青蛙” 查找文章—java并发性能(五)之牛逼的AQS(上)。特此申明!
我们之前都是直接在线程中使用各种同步机制,我们可以把相关的同步问题抽象出来单独定义一些工具,这些工具可以在合适的并发场景下复用,下边我们看如何自定义我们自己的同步工具
。
设计java的大叔们为了我们方便的自定义各种同步工具,为我们提供了大杀器AbstractQueuedSynchronizer
类,这是一个抽象类,以下我们会简称AQS
,翻译成中文就是抽象队列同步器
。这家伙老有用了,封装了各种底层的同步细节,我们程序员想自定义自己的同步工具的时候,只需要定义这个类的子类并覆盖它提供的一些方法就好了。我们前边用到的显式锁ReentrantLock
就是借助了AQS
的神力实现的,现在马上来看看这个类的实现原理以及如何使用它自定义同步工具。
1.同步状态
在AQS
中维护了一个名叫state
的字段,是由volatile
修饰的,它就是所谓的同步状态
:
private volatile int state;
并且提供了几个访问这个字段的方法:
方法名 | 描述 |
protected final int getState( ) | 获取state的值 |
protected final void setState( int newState ) | 设置state的值 |
protected final boolean compareAndSetState( int expect, int update ) | 使用CAS方式更新state的值 |
可以看到这几个方法都是final
修饰的,说明子类中无法重写它们。另外它们都是protected
修饰的,说明只能在子类中使用这些方法。
在一些线程协调的场景中,一个线程在进行某些操作的时候其他的线程都不能执行该操作,比如持有锁时的操作,在同一时刻只能有一个线程持有锁,我们把这种情景称为独占模式
;在另一些线程协调的场景中,可以同时允许多个线程同时进行某种操作,我们把这种情景称为共享模式
。
我们可以通过修改state
字段代表的同步状态
来实现多线程的独占模式
或者共享模式
。
比如在独占模式
下,我们可以把state
的初始值设置成0
,每当某个线程要进行某项独占
操作前,都需要判断state
的值是不是0
,如果不是0
的话意味着别的线程已经进入该操作,则本线程需要阻塞等待;如果是0
的话就把state
的值设置成1
,自己进入该操作。这个先判断再设置的过程我们可以通过CAS
操作保证原子性,我们把这个过程称为尝试获取同步状态
。如果一个线程获取同步状态
成功了,那么在另一个线程尝试获取同步状态
的时候发现state
的值已经是1
了就一直阻塞等待,直到获取同步状态
成功的线程执行完了需要同步的操作后释放同步状态
,也就是把state
的值设置为0
,并通知后续等待的线程。
在共享模式
下的道理也差不多,比如说某项操作我们允许10
个线程同时进行,超过这个数量的线程就需要阻塞等待。那么我们就可以把state
的初始值设置为10
,一个线程尝试获取同步状态
的意思就是先判断state
的值是否大于0
,如果不大于0
的话意味着当前已经有10个线程在同时执行该操作,本线程需要阻塞等待;如果state
的值大于0
,那么可以把state
的值减1
后进入该操作,每当一个线程完成操作的时候需要释放同步状态
,也就是把state
的值加1
,并通知后续等待的线程。
所以对于我们自定义的同步工具来说,需要自定义获取同步状态与释放同步状态的方式,而AQS
中的几个方法正是用来做这个事儿的:
方法名 | 描述 |
protected boolean tryAcquire( int arg ) | 独占式的获取同步状态,获取成功返回true,否则false |
protected boolean tryRelease( int arg ) | 独占式的释放同步状态,释放成功返回true,否则false |
protected boolean tryAcquireShared( int arg ) | 共享式的获取同步状态,获取成功返回true,否则false |
protected boolean tryAcquireShared( int arg ) | 共享式的释放同步状态,释放成功返回true,否则false |
protected boolean isHeldExclusively( ) | 在独占模式下,如果当前线程已经获取到同步状态,则返回true; 其他情况则返回fasle |
我们说AQS
是一个抽象类,我们以tryAcquire
为例看看它在AQS
中的实现:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
我的天,竟然只是抛出个异常,这不科学。是的,在AQS
中的确没有实现这个方法,不同的同步工具针对的具体并发场景不同,所以如何获取同步状态和如何释放同步状态是需要我们在自定义的AQS
子类中实现的,如果我们自定义的同步工具需要在独占模式
下工作,那么我们就重写tryAcquire
、tryRelease
和isHeldExclusively
方法,如果是在共享模式
下工作,那么我们就重写tryAcquireShared
和tryReleaseShared
方法。比如在独占模式下我们可以这样定义一个AQS
子类:
public 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;
}
}
tryAcquire
表示尝试获取同步状态
,我们这里定义了一种极其简单的获取方式,就是使用CAS
的方式把state
的值设置成1,如果成功则返回true
,失败则返回false
,tryRelease
表示尝试释放同步状态
,这里同样采用了一种极其简单的释放算法,直接把state
的值设置成0
就好了。isHeldExclusively
就表示当前是否有线程已经获取到了同步状态。如果你有更复杂的场景,可以使用更复杂的获取和释放算法来重写这些方法。
通过上边的唠叨,我们只是了解了啥是个同步状态
,学会了如何通过继承AQS
来自定义独占模式和共享模式下获取和释放同步状态的各种方法,但是你会惊讶的发现会了这些仍然没有什么卵用。我们期望的效果是一个线程获取同步状态成功会立即返回true
,并继续执行某些需要同步的操作,在操作完成后释放同步状态,如果获取同步状态失败的话会立即返回false
,并且进入阻塞等待状态,那线程是怎么进入等待状态的呢?不要走开,下节更精彩。
2. 同步队列
AQS
中还维护了一个所谓的同步队列
,这个队列的节点类
被定义成了一个静态内部类,它的主要字段如下
static final class Node {
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
}
AQS
中定义一个头节点引用
,一个尾节点引用
:
private transient volatile Node head;
private transient volatile Node tail;
通过这两个节点就可以控制到这个队列,也就是说可以在队列上进行诸如插入和移除操作。可以看到Node
类中有一个Thread
类型的字段,这表明每一个节点都代表一个线程。我们期望的效果是当一个线程获取同步状态失败之后,就把这个线程阻塞并包装成Node
节点插入到这个同步队列
中,当获取同步状态成功的线程释放同步状态的时候,同时通知在队列中下一个未获取到同步状态的节点,让该节点的线程再次去获取同步状态。
这个节点类
的其他字段的意思我们之后遇到会详细唠叨,我们先看一下独占模式
和共享模式
下在什么情况下会往这个同步队列
里添加节点,什么情况下会从它里边移除节点,以及线程阻塞和恢复的实现细节。
3. 独占式同步状态获取与释放
在独占模式
下,同一个时刻只能有一个线程获取到同步状态,其他同时去获取同步状态的线程会被包装成一个Node
节点放到同步队列
中,直到获取到同步状态的线程释放掉同步状态才能继续执行。初始状态的同步队列
是一个空队列,里边一个节点也没有,就长这样:
接下来我们就要详细看一下获取同步状态失败的线程是如何被包装成Node
节点插入到队列中同时阻塞等待的。
前边说过,获取和释放同步状态的方式是由我们自定义的,在独占模式
需要我们定义AQS
的子类并且重写下边这些方法:
protected boolean tryAcquire(int arg)
protected boolean tryRelease(int arg)
protected boolean isHeldExclusively()
在定义了这些方法后,谁去调用它们呢?AQS
里定义了一些调用它们的方法,这些方法都是由public final
修饰的:
忽然摆了这么多方法可能有点突兀哈,我们先看一下acquire
方法的源代码:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
代码显示acquire
方法实际上是通过tryAcquire
方法来获取同步状态的,如果tryAcquire
方法返回true
则结束,如果返回false
则继续执行。这个tryAcquire
方法就是我们自己规定的获取同步状态的方式。假设现在有一个线程已经获取到了同步状态,而线程t1
同时调用tryAcquire
方法尝试获取同步状态,结果就是获取失败,会先执行addWaiter
方法,我们一起来看一下这个方法:
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)) { //更新tail,并且把新节点插入到列表最后
pred.next = node;
return node;
}
}
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { //tail节点为空,初始化队列
if (compareAndSetHead(new Node())) //设置head节点
tail = head;
} else { //tail节点不为空,开始真正插入节点
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
可以看到,这个addWaiter
方法就是向队列中插入节点的方法。首先会构造一个Node
节点,假设这个节点为节点1
,它的thread
字段就是当前线程t2
,这个节点被刚刚创建出来的样子就是这样:
然后我们再分析一下具体的插入过程。如果tail
节点不为空,直接把新节点插入到队列后边就返回了,如果tail
节点为空,调用enq
方法先初始化一下head
和tail
节点之后再把新节点插入到队列后边。enq
方法的这几行初始化队列的代码需要特别注意:
if (t == null) { //tail节点为空,初始化队列
if (compareAndSetHead(new Node())) //设置head节点
tail = head;
} else {
//真正插入节点的过程
}
也就是说在队列为空的时候会先让head
和tail
引用指向同一个节点后再进行插入操作,而这个节点竟然就是简简单单的new Node()
,真是没有任何添加剂呀~ 我们先把这个节点称为0号节点吧
,这个节点的任何一个字段都没有被赋值,所以在第一次节点插入后,队列
其实长这样:
其中的节点1
才是我们真正插入的节点,代表获取同步状态失败的线程,0号节点
是在初始化过程中创建的,我们之后再看它有什么用。
addWaiter
方法调用完会返回新插入的那个节点,也就是节点1
,acquire
方法会接着调用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; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
可以看到,如果新插入的节点的前一个节点是头节点
的话,会再次调用tryAcquire
尝试获取同步状态,这个主要是怕获取同步状态的线程很快就把同步状态给释放了,所以在当前线程阻塞之前抱着侥幸的心理再试试能不能成功获取到同步状态,如果侥幸可以获取,那就调用setHead
方法把头节点
换成自己:
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
同时把本Node
节点的thread
字段设置为null
,意味着自己成为了0号节点
。
如果当前Node
节点不是头节点或者已经获取到同步状态的线程并没有释放同步状态,那就乖乖的往下执行shouldParkAfterFailedAcquire
方法:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; //前一个节点的状态
if (ws == Node.SIGNAL) //Node.SIGNAL的值是-1
return true;
if (ws > 0) { //当前线程已被取消操作,把处于取消状态的节点都移除掉
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else { //设置前一个节点的状态为-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
这个方法是对Node
节点中的waitStatus
的各种操作。如果当前节点的前一个节点的waitStatus
是Node.SIGNAL
,也就是-1,那么意味着当前节点可以被阻塞,如果前一个节点的waitStatus
大于0
,意味着该节点代表的线程已经被取消操作了,需要把所有waitStatus
大于0
的节点都移除掉,如果前一个节点的waitStatus
既不是-1
,也不大于0
,就把如果前一个节点的waitStatus
设置成Node.SIGNAL
。我们知道Node
类里定义了一些代表waitStatus
的静态变量,我们来看看waitStatus
的各个值都是什么意思吧:
未完待续!