从Lock到AQS了解Java中的锁
本文主要会从 Lock 接口到 AQS 抽象类的 API 以及源码分析 Java 中锁的实现,通过演示相关组件的代码 Demo 了解其使用,以及了解如何通过 AQS 实现一个锁。
Lock Interface
锁是我们在开发问题中解决并发问题的常见手段,而 Lock 接口及其实现子类可能是我们工作中经常涉及到的一种保证同步的一种方式,那 Lock 是什么呢?
Lock implementations provide more extensive locking operations than can be obtained using synchronized methods and statements. They allow more flexible structuring, may have quite different properties, and may support multiple associated Condition objects.
我们知道最基础的线程同步方式可以使用 synchronized 去修饰需要同步的代码块,并且随着 JDK 1.6 之后引入了锁膨胀的机制,在某些情况下也并不是一个重量级锁,但是相较于 Lock 接口的实现子类,它却显得不是很灵活。而 Lock 接口的实现子类最大的优势就是使用起来十分灵活且便于扩展。我们首先来看下 Lock 接口的定义:
public interface Lock {
// Acquires the lock.
void lock();
// Acquires the lock unless the current thread is interrupted.
void lockInterruptibly();
// Acquires the lock only if it is free at the time of invocation.
boolean tryLock();
// Acquires the lock if it is free within the given waiting time
// and current thread has not been interrupt
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// Releases the lock
void unlock();
// Return a new Condition instance that is bound to this instance.
Condition newCondition();
}
接口的定义很简单,我们先不去了解最后的 newCondition()
函数,其他几个函数的定义我们见名知义,分别就是获取锁以及释放锁。
我们可以来看一下实现 Lock 接口的类,较为常见的有以下几个:
- ReentrantLock:重入锁,实现了 Lock 接口。重入是指线程在获取锁之后,再次获取该锁不需要阻塞,而是直接关联一次计数器增加重入次数;
- ReentrantReadWriteLock:重入读写锁,实现 ReadWriteLock 接口,该类中维护了两个锁——ReadLock & WriteLock,它们都分别实现了 Lock 接口。其适用在读多写少的情况下,其基本原则是:读和读不互斥、读和写互斥、写和写互斥,即涉及到影响数据的操作都会存在互斥;
- StampedLock:JDK 1.8 引入的一种新的锁机制,可以认为是读写锁的一个改进版本。读写锁通过分离读和写使得读和读之间可以进行并发操作,但是由于读和写还是有冲突,所以当存在大量读线程时可能造成写线程的饥饿。StampedLock 改用乐观锁思想实现读策略,使得写操作不会被阻塞。
接下来我们以一段 Demo 来演示 ReentrantLock 的简单使用:
public class ReentrantLockDemo {
private static int count = 0;
static Lock lock = new ReentrantLock();
static CountDownLatch countDownLatch = new CountDownLatch(3);
public static void incr() {
// acquires the lock
lock.lock();
try {
Thread.sleep(1);
countDownLatch.countDown();
count++;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// release
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
new Thread(ReentrantLockDemo::incr).start();
}
countDownLatch.await();
System.out.println(count);
}
}
上面这段代码通过 ReentrantLock 实现了incr()
中 count++ 做递增的这一操作,如果这一函数没有实现同步,那么一定存在线程安全问题。所以用到了 ReentrantLock 来作为一个同步锁实现了线程安全,在进行 count++ 操作之前进行加锁,之后再在 finally 中释放锁。
而 Lock 接口的功能实现基本上都是通过聚合一个同步器的内部子类来完成线程的访问控制的,接下来我们就来了解这个同步器,也就是 AQS。
AQS
我们需要先考虑一个问题,多个线程并发争夺锁资源,那么 Lock 接口的实现类是如何保证竞争失败的线程的等待以及后续的唤醒呢?带着这个问题,我们来了解一下什么是 AQS。
什么是 AQS
AQS (Abstract Queued Synchronizer),它提供了一个 FIFO 队列来使得资源获取线程的排队工作,同时使用了一个 int 成员变量表示同步状态。其本身没有实现任何的同步接口,经常被作为其他实现了 Lock 接口的类用来实现同步锁以及涉及到其他同步功能的核心组件,类似于上述所提及到的 ReentrantLock,它便利用了 AQS。所以了解 AQS 是了解 JUC 必不可少的一部分知识。
Doug Lea 在设计 AQS 时,期望其能成为实现大部分同步需求的基础,所以 AQS 的设计也是基于”模板方法模式“的。那么对于继承了 AQS 的类来说,需要去重写指定的方法以实现我们需要的功能,之后再由组合了 AQS 的同步组件去调用。
我们来看 AQS 提供的可重写的方法:
protected boolean tryAcquire(int arg) : 独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行 CAS 设置同步状态;
protected boolean tryRelease(int arg) : 独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态;
protected int tryAcquireShared(int arg) : 共享式获取同步锁,返回大于等于 0 的值,表示获取成功,反之,获取失败;
protected boolean tryReleaseShared(int arg) : 共享式释放同步状态;
protected boolean isHeldExclusively() : 当前同步锁是否在独占模式下被线程占用,一般该方法表示是否被当前线程所占。
从使用层面上来说,AQS 的功能分为两种:独占和共享
- 独占锁,每次只能有一个线程持有锁,比如 ReentrantLock 就是以独占的方式实现互斥锁;
- 共享锁,允许多个线程同时获取锁,并发访问共享资源,比如 ReentrantReadWriteLock;
除此之外,重写同步方法时,需要使用同步器提供的如下三个方法来访问或修改同步状态。
- getState() : 获取当前同步状态;
- setState(int newState) : 设置当前同步状态;
- compareAndSetState(int expect, int update) : 使用 CAS 设置当前状态,该方法能够保证状态设置的原子性。
接下来,我们就以文档中提供的示例代码做说明:
public class Mutex implements Lock, Serializable {
// 静态内部类,继承 AQS 并重写其中方法
private static class Sync extends AbstractQueuedSynchronizer {
// Reports whether in locked state
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
// Acquire the lock if state is zero
public boolean tryAcquire(int acquires) {
assert acquires == 1; // Otherwise unused
if (compareAndSetState(0, 1)) { // 通过 CAS 设置
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// Release the lock by setting state to zero
protected boolean tryRelease(int releases) {
assert releases == 1; // Otherwise Unused
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
// Providers a Condition
Condition newCondition() {
return new ConditionObject();
}
// Deserializes properly
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}
// 将操作代理到 Sync 上
private final Sync sync = new Sync();
public void lock() { sync.acquire(1); }
public boolean tryLock() { return sync.tryAcquire(1); }
public void unlock() { sync.release(1); }
public Condition newCondition() { return sync.newCondition(); }
public boolean isLocked() { return sync.isHeldExclusively();}
public boolean hasQueuedThreads() { return sync.hasQueuedThreads();}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
}
这样一个简单的独占锁就实现了。外部使用 Mutex 时并不会感知到 AQS 的存在,而是直接调用 Mutex 所提供的方法。而我们在实现 Mutex 时,也并不需要去关注如何实现释放锁,获取锁的逻辑,只需要将其委托给 AQS 的实现类即可,这便降低了实现一个可靠自定义同步组件的门槛。
AQS 的实现原理
知道如何依靠 AQS 实现一个同步组件后,我们再回到我们最先提出的问题。Lock 接口的实现类,或者说 AQS 如何保证竞争失败的线程的等待以及后续的唤醒呢?
AQS 的实现依赖内部的同步队列,也就是 FIFO 的双向队列,如果当前线程竞争锁失败,那么 AQS 会把当前线程以及等待状态信息构造成一个 Node 节点并加入到同步队列中,同时再阻塞该线程。当获取锁的线程释放后,会从队列中唤醒一个阻塞节点(线程)。
我们先来了解构造出来的 Node 的结构:
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; //存储在condition队列中的后继节点
// 是否为共享锁
final boolean isShared() {
return nextWaiter = SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointException();
else
return p;
}
Node() { // Used to establish inital head or SHARED marker
}
// 将线程构造成一个 Node,添加等待队列
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;
}
}
当有一个新的线程无法获取到同步状态,那么就要根据此线程构造一个新的节点并加入到队列中,加入队列的这个过程是有并发安全问题的,所以需要使用 AQS 提供的一个基于 CAS 的设置尾结点的方法:compareAndSetTail(Node expect, Node update)
,这个过程如下所示:
这一部分的代码逻辑如下:
for (;;) {
Node t = tail;
if (t == null) { // 没有尾结点
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) { // CAS 替换尾结点
t.next = node; // 将先前的尾结点的后置节点设置为现尾结点
return t;
}
}
}
有线程获取锁,那么同样的也会有线程释放锁。在 AQS 中,head 节点表示获取同步状态成功的节点,当 head 节点在释放同步状态时,会唤醒后继节点,如果后继节点获取成功,那么会将自己设置为头节点,这一步骤由于只有一个线程能够成功得获取到同步状态,所以并不需要使用 CAS 来保证,只需将首结点设置为原首节点的后继节点并断开原首节点的 next 引用即可。
AQS 源码分析
从上面了解完什么是 AQS 以及 AQS 的最基本原理,我们再来通过 AQS 分析 AQS 的具体执行逻辑。我们从 ReentrantLock 入手,ReentrantLock 中公平锁和非公平锁在底层的实现原理是相通(后面会举例),所以这里我们用非公平锁举例,源码之间的调用过程我们利用时序图的方式来展现:
锁的获取
由上图可知,如果当锁调用失败时,会调用 addWaiter()
方法将当前线程封装成 Node 节点加入到 AQS 队列,那我们就以这个逻辑看下从 ReentrantLock 到 addWaiter()
方法中的实现:
java.util.concurrent.locks.ReentrantLock
public void lock() {
sync.lock();
}
这里是获取锁的入口,调用了 sync 这个类里面的方法,这里的 sync 和我们上面演示的代码一样,也是一个用来帮助实现同步组件的静态类:
abstract static class Sync extends AbstractQueuedSynchronizer {
// ...
abstract void lock();
// ...
}
Sync 是一个抽象类,它有两个具体的实现类,分别是 NofairSync(非公平锁),FairSync(公平锁):
- 公平锁:表示所有的线程严格按照 FIFO 来获取锁;
- 非公平锁:表示可以存在抢占锁的功能,也就是说不管当前节点之前是否存在其他线程等待,当前节点都有机会抢占锁。
我们来看下 NofairSync 对 Sync 中 Lock 的实现:
static final class NonfairSync extends Sync {
// ...
final void lock() {
if (compareAndSetState(0, 1)) // 通过 CAS 操作来修改 state 状态
setExclusiveOwnerThread(Thread.currentThread()); // 争抢成功则修改获得锁状态的线程
else
acquire(1);
}
}
在讲解这段代码之前,我们需要先来了解 state
, 在 AQS 中,利用 state 来表示锁的状态(不同的 AQS 实现,state 所表达的含义是不一样的),这里以 ReentrantLock 举例:
- state = 0:表示无锁状态;
- state > 0:表示已有线程获得了锁,如果只有一个线程获得了锁,此时 state 可能为 1,但是 ReentrantLock 允许重入,所以当同一线程多次获取到同步锁时,state 会递增,比如重入了 5 次,那么 state = 5。而在释放锁的时候,也需要等到 state 递减到 0 的时候才能释放锁。
回到上面这段代码,这里的主要逻辑是:当调用 lock 方法时,想通过 CAS 去抢占锁(即修改 state 的状态),如果抢占失败,将获得锁状态的线程设置为当前线程,如果抢占失败,那么就通过调用 acquire()
来继续下面的逻辑。
java.util.concurrent.locks.AbstractQueuedSynchronizer
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire
是 AQS 中的方法,如果 CAS 操作没有成功,说明当前锁已经被有线程占有了,acquire
中的逻辑主要是:
- 通过
tryAcquire
尝试获取独占锁,如果成功返回 true,失败返回 false; - 如果
tryAcquire
失败,则会通过addWaiter
方法将当前线程封装成 Node 添加到 AQS 队列尾部; acquireQueued
,将 Node 作为参数,通过自旋去尝试获取锁。
接下来就依次介绍这两个方法,tryAcquire
,这个方法的作用是尝试获取锁,如果成功返回 true,不成功返回 false。
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
// 获得当前执行的线程
final Thread current = Thread.currentThread();
// 获取 state 的值
int c = getState();
if (c == 0) { // state=0 说明当前是无锁状态
// 通过 CAS 操作来替换 state 的值改为1
if (compareAndSetState(0, acquires)) {
// 保存当前获得锁的线程
setExclusiveOwnerThread(current);
return true;
}
}
// 如果是同一个线程来获取锁,则直接增加重入次数
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires; // 增加重入次数
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
这两部分的代码的逻辑:
- 获取当前线程,判断当前的锁的状态;
- 如果 state = 0 表示当前是无锁状态,通过 CAS 更新 state 状态的值;
- 如果当前线程是输属于重入,则增加重入次数。
而当 tryAcquire
方法获取锁失败以后,则会先调用 addWaiter
将当前线程封装成 Node,然后添加到 AQS 队列:
private Node addWaiter(Node mode) { // mode=Node.EXCLUSIVE
//将当前线程封装成Node,并且mode为独占锁
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// tail是AQS的中表示同步队列队尾的属性,刚开始为null,所以进行enq(node)方法
Node pred = tail;
if (pred != null) { //tail不为空的情况,说明队列中存在节点数据
node.prev = pred; //讲当前线程的Node的prev节点指向tail
if (compareAndSetTail(pred, node)) {//通过cas讲node添加到AQS队列
pred.next = node;//cas成功,把旧的tail的next指针指向新的tail
return node;
}
}
enq(node); //tail=null,将node添加到同步队列中
return node;
}
该方法的主要逻辑如下:
- 将当前线程封装成 Node;
- 判断当前链表中的 tail 节点是否为空,如果不为空,则通过 CAS 操作把当前线程的 Node 添加到 AQS 队列
- 如果为空或者 CAS 失败,则调用
enq(final Node node)
将节点添加到 AQS 队列;
private Node enq(final Node node) {
// 自旋,知道成功将节点添加到队列
for (;;) {
Node t = tail; // 如果是第一次添加到队列,那么tail=null
if (t == null) { // Must initialize
//CAS的方式创建一个空的Node作为头结点
if (compareAndSetHead(new Node()))
//此时队列中只一个头结点,所以tail也指向它
tail = head;
} else {
//进行第二次循环时,tail不为null,进入else区域。将当前线程的Node结点的prev指向tail,
// 然后使用CAS将tail指向Node
node.prev = t;
if (compareAndSetTail(t, node)) {
// t此时指向tail,所以可以CAS成功,将tail重新指向Node。此时t为更新前的tail的值,即指向空的头结点,
// t.next=node,就将头结点的后续结点指向Node,返回头结点
t.next = node;
return t;
}
}
}
}
至此,就能成功将没有抢占到锁的线程封装到 Node 中并加入到 AQS 队列上。接下来我们来了解 acquireQueued
方法的主要逻辑,在这里面会做抢占锁的操作:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();// 获取prev节点,若为null即刻抛出NullPointException
if (p == head && tryAcquire(arg)) {// 如果前驱为head才有资格进行锁的抢夺
setHead(node); // 获取锁成功后就不需要再进行同步操作了,获取锁成功的线程作为新的head节点
// 凡是head节点,head.thread与head.prev永远为null, 但是head.next不为null
p.next = null; // help GC
failed = false; //获取锁成功
return interrupted;
}
// 如果获取锁失败,则根据节点的waitStatus决定是否需要挂起线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) // 若前面为true,则执行挂起,待下次唤醒的时候检测中断的标志
interrupted = true;
}
} finally {
if (failed) // 如果抛出异常则取消锁的获取,进行出队(sync queue)操作
cancelAcquire(node);
}
}
- 获取当前节点的 prev 节点;
- 如果 prev 节点为 head 节点,那么它就有资格去争抢锁,调用
tryAcquire
抢占锁; - 抢占锁成功以后,把获得锁的节点设置为 head,并且移除原来的初始化 head 节点;
- 如果获取锁失败,则根据 waitStatus 决定是否需要挂起线程;
- 最后,通过
cancelAcquire
取消获取锁的操作;
前面的逻辑都很好理解,主要看一下shouldParkAfterFailedAcquire
这个方法和parkAndCheckInterrupt
的作用:
从上面代码的分析可以看出,只有队列的第二个节点才可以有机会征用锁,如果成功获取锁,则此节点晋升为头节点。对于第三个及以后的节点,if (p == head)条件不成立,首先进行shouldParkAfterFailedAcquire(p, node)
操作。
shouldParkAfterFailedAcquire
方法判断一个争用锁的线程是否应该被阻塞。它首先判断一个节点的前置节点的状态是否为 Node.SIGNAL
,如果是,说明前置节点已经将状态设置为”如果锁释放,则应当通知它,所以它可以安全地阻塞了“,返回true。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; //前继节点的状态
if (ws == Node.SIGNAL)//如果是SIGNAL状态,意味着当前线程需要被unpark唤醒
return true;
// 如果前节点的状态大于0,即为CANCELLED状态时,则会从前节点开始逐步循环找到一个没有被“CANCELLED”节点设置为当前节点的前节点,返回false。
// 在下次循环执行shouldParkAfterFailedAcquire时,返回true。这个操作实际是把队列中CANCELLED的节点剔除掉。
if (ws > 0) {// 如果前继节点是“取消”状态,则设置 “当前节点”的 “当前前继节点” 为 “‘原前继节点'的前继节点”。
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else { // 如果前继节点为“0”或者“共享锁”状态,则设置前继节点为SIGNAL状态。
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
如果shouldParkAfterFailedAcquire返回了true,则会执行:parkAndCheckInterrupt()方法,它是通过LockSupport.park(this)将当前线程挂起到WATING状态,它需要等待一个中断、unpark方法来唤醒它,通过这样一种FIFO的机制的等待,来实现了Lock的操作。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
LockSupport类是Java6引入的一个类,提供了基本的线程同步原语。LockSupport实际上是调用了Unsafe类里的函数,归结到Unsafe里,只有两个函数:
public native void unpark(Thread jthread);
public native void park(boolean isAbsolute, long time);
unpark
函数为线程提供 “许可(permit)”,线程调用park函数则等待“许可”。这个有点像信号量,但是这个“许可”是不能叠加的,“许可”是一次性的。
permit 相当于 0/1 的开关,默认是 0,调用一次 unpark 就加 1 变成了 1。调用一次 park 会消费 permit,又会变成0。 如果再调用一次 park 会阻塞,因为permit 已经是0了。直到permit变成 1。这时调用 unpark 会把 permit 设置为 1。每个线程都有一个相关的 permit,permit 最多只有一个,重复调用 unpark 不会累积。
锁的释放
加锁的过程分析完以后,再来分析一下释放锁的过程,释放锁需要调用 release 方法,这个方法里面做两件事:1、释放锁 ;2、唤醒park的线程
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
这个动作可以认为就是一个设置锁状态的操作,而且是将状态剪掉传入的参数值(参数是1),如果结果状态为0,就将排它锁的 Owner 设置为null,以使得其它的线程有机会进行执行。
在排它锁中,加锁的时候状态会增加1(当然可以自己修改这个值),在解锁的时候减掉1,同一个锁,在可以重入后,可能会被叠加为2、3、4这些值,只有unlock()
的次数与 lock()
的次数对应才会将 Owner 线程设置为空,而且也只有这种情况下才会返回 true。
protected final boolean tryRelease(int releases) {
int c = getState() - releases; // 这里是将锁的数量减1
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
// 由于重入的关系,不是每次释放锁c都等于0
// 直到最后一次释放锁时,才会把当前线程释放
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
在方法 unparkSuccessor(Node)
中,就意味着着真正要释放锁了,它传入的是head节点(head节点是占用锁的节点),当前线程被释放之后,需要唤醒下一个节点的线程:
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
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)
//然后从队列尾部向前遍历找到最前面的一个waitStatus小于0的节点
//至于为什么从尾部开始向前遍历
//因为在 doAcquireInterruptibly.cancelAcquire 方法的处理过程中只设置了next的变化,没有设置prev的变化
//在最后有这样一行代码:node.next = node
//如果这时执行了unparkSuccessor方法,并且向后遍历的话,就成了死循环了,所以这时只有 prev 是稳定的
if (t.waitStatus <= 0)
s = t;
}
//内部首先会发生的动作是获取head节点的next节点
//如果获取到的节点不为空,则直接通过:“LockSupport.unpark()”方法来释放对应的被挂起的线程
//这样一来将会有一个节点唤醒后继续进入循环进一步尝试tryAcquire()方法来获取锁
if (s != null)
LockSupport.unpark(s.thread); //释放许可
}
参考资料
-
Lea D. The java. util. concurrent synchronizer framework[J]. Science of Computer Programming, 2005, 58(3): 293-309.
-
《Java并发编程的艺术》