1 从ReentrantLock说起
Java从语言级别支持多线程,用户可以很容易的编写多线程的程序。需要注意的是,当多个线程访问共享资源时,为了保证数据的正确性,需要进行同步。Java提供了synchronized关键字(内置锁),synchronized块和synchronized方法可以用来进行同步。与此同时,java.util.concurrent包中还提供了ReentrantLock,它实现了Lock接口,提供了与synchronized相同的互斥性和内存可见性。
那么ReentrantLock是如何实现的?通过源码来看一下:
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
...
}
public void lock() {
sync.lock();
}
public void unlock() {
sync.release(1);
}
}
可以看到,ReentrantLock有Sync类型的成员变量sync,而lock()方法和release()方法的实现也是通过委托给sync的同名方法来实现的。Sync继承了一个名为AbstracQueueSynchronizer的类(简称AQS),AQS就是ReentrantLock实现的基石。
2 AQS简介
AQS为实现依赖于先进先出(FIFO)等待队列的同步器提供了一个框架,它的设计目标是成为那些依靠单个int值来表示状态的大多数同步器的基石。
同步器有两种方法:至少一个acquire操作--它会使调用线程阻塞除非/直到同步状态允许该线程继续执行。至少一个release操作--它对同步状态的改变可能会让一个或多个被阻塞的线程解除阻塞。
java.util.concurrent包没有为同步器定义一个单一的统一接口。有些同步器通过公共接口定义(例如Lock),但是其他的只包括专门的版本。所以,不同的类的acquire和release操作的方法名不尽相同。例如,Lock.lock,Semaphore.acquire,CountDownLatch.await以及FutureTask.get对应的都是框架中的acquire操作。
对于是否只维护独享状态这一点来说,不同的同步器之间可能存在区别。对于只维护独享状态的同步器来说,同一时刻只能有一个线程继续通过阻塞点。而对于支持共享状态的同步器来说,可以有多个线程同时执行。一般的锁类当然只维护独享状态,但是诸如信号量这样的同步器,可以被指定数量的线程同时获取。基于这一点,AQS同时同时支持两种模式。
3 AQS设计要点和实现原理
- 原子地管理同步状态
使用一个变量来表示同步器的同步状态。例如,可重入锁处于锁定状态还是释放状态(0表示未锁定,n表示重入的次数)。此外,由于state是会被多线程并发访问的共享数据,因此对state的操作需要是原子的。
-
线程的阻塞和唤醒
多线程并发访问时,如果产生争用,对于获取操作失败的线程,要进行阻塞。而当持有锁的线程释放锁后,需要把阻塞的线程唤醒,继续执行。
- 队列的维护
对于进入阻塞状态的线程,需要使用一个队列进行保存,以便后续的唤醒操作。因为该队列也会被并发访问,因此它需要是线程安全的。
3.1 同步状态
// 同步状态
private volatile int state;
// 返回同步状态的当前值,此操作有volatile读的内存语义
protected final int getState() {
return state;
}
// 设置同步状态的值,此操作有volatile写的内存语义
protected final void setState(int newState) {
state = newState;
}
// 如果同步状态的当前值和预期值相同,就原子地将同步状态的值设为指定的值
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
3.2 阻塞与唤醒
AQS使用LockSupport的park和unpark方法实现线程的阻塞和唤醒。
LockSupport.park方法阻塞当前线程,除非/直到LockSupport.unpark被执行。对unpark的调用不会被计数,所以park之前的多个unpark只会解除阻塞一个park。此外,操作是针对每个线程的,而非每个同步器。一个线程在一个新的同步器上调用park可能会立即返回–因为之前可能曾经对这个线程调用过unpark。如果没有调用过unpark,则下一次的park调用会阻塞。
park操作支持可选的相对或绝对的过期时间,并且与Thread.interrupt相集成---中断一个线程时,会unpark该线程。
3.3 队列
队列的维护是AQS的核心。AQS使用的是先进先出(FIFO)的队列,所以它不支持基于优先级的同步。该队列是CLH队列的变体,它是一个非阻塞的数据结构,队列自身不需要使用更底层的锁来构建。
Node类的成员:
- prev:指向前驱节点
- next:指向后继节点
- waitStatus:节点状态
- thread:将节点入队的线程
- nextWaiter:指向条件队列上的下一个节点
阻塞线程队列和条件等待队列中的节点类型都是Node(prev和next被阻塞线程队列使用,而nextWaiter被条件等待队列使用),当执行signal操作时,条件队列中的节点会被移动到阻塞线程队列中。
入队操作:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
入队时使用CAS操作, 即便有争用,也总有一个线程会成功插入,所以一定会有进展。
出队操作:
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
出队时,只需将head指向当前线程所对应的节点,并把thread和prev置null即可。
waitStatus的取值:
-
CANCELLED:1 已取消,等待超时或被中断
-
SIGNAL:-1 后继节点被阻塞,需要唤醒
-
CONDITION:-2 节点在条件队列中
-
PROPAGATE:-3 保证共享模式下release操作能够向后传播下去
-
默认值:0
节点状态默认值是0,当新节点入队时,在调用park之前,要保证前一个节点的waitStatus被设置成了SIGNAL。当节点取消对锁的等待,或者已经等待超时了,则waitStatus的值会被设为CANCELLED。条件队列中的节点的waitStatus是CONDITION。共享模式下,release操作时可能会把waitStatus设置为PROPAGATE。
独占模式下的acquire&release
acquire流程图
release流程图
4 与内置锁的比较
相比于Java内置的同步机制,AQS有一些优势:
- 可中断的等待
- 提供了除互斥锁之外的其他同步器
- 便于扩展
- 提供了公平性的选择
5 其他
模板方法模式
AQS定义了tryAcquire,tryRelease等protect方法,子类根据需要实现对应的方法,而AQS的acquire,release方法会分别调用tryAcquire,tryRelease,根据他们的返回值进行相应的处理。
方法空实现
AQS支持独占模式和共享模式,一般子类只需实现一种即可,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();
}
这样一来,需要实现tryAcquire和tryRelease的子类就不需要自己实现tryAcquireShared和tryReleaseShared了。
功能委托
ReentrantLock并没有直接继承AQS,而是通过内部类Sync继承AQS,再把lock和unlock的功能委托给Sync,这样减少了ReentrantLock和AQS的耦合。
其他同步器,例如,CountDownLatch,Semaphore,FutureTask也都是类似的。
6 结语
AQS作为java.util.concurrent包中的核心类,对同步器的共性进行了抽象,为各种同步器的实现提供了基础。它提供了一些内置同步机制所不具有的特性,当内置同步机制不能满足需求时,可以使用基于AQS的同步器作为必要的补充。