构造线程安全类时常用的一个策略是将线程安全委托给现有的线程安全类,Java平台类库包含了丰富的线程安全类,包含同步容器类、同步工具类。这些同步容器类、同步工具类中,很多底层实现都是AQS,所以,本文将先介绍AQS,再分别介绍各种工具类。
线程的状态
状态分类
线程共有6种状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED
其中,RUNNABLE状态包括 【运行中】 和 【就绪】;BLOCKED(阻塞态)状态只有在【等待进入synchronized方法(块)】和 【其他Thread调用notify()或notifyAll(),但是还未获得锁】才会进入;

sleep、yield、join与wait、notify的区别
Thread类提供了这6种方法,但调用机制不同:
sleep、yield、join调用的Thread的方法,只放弃cpu,不放弃锁。
wait、notify、notifyAll调用的Object的方法,不仅放弃cpu,也会释放对象锁。
- sleep(long millis),一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,但不释放对象锁,millis后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式。
- yield(),一定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。
- join()/join(millis),当前线程t1调用其它线程t2的join方法,当前线程进入WAITING/TIMED_WAITING状态,当前线程不会释放已经持有的对象锁。线程t2执行完毕或者millis时间到,当前线程t1进入就绪状态。根据happens-before规则,t1线程执行join()后的代码时能获取到t2线程对变量的修改。
- wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout) timeout时间到自动唤醒。
- notify(),唤醒在此对象监视器上等待的单个线程,选择是任意性的。
- notifyAll()唤醒在此对象监视器上等待的所有线程。
AQS
前言
AQS是AbstractQueuedSynchronizer的简称。大多数开发者不会使用到AQS,jdk提供了基于AQS的丰富的工具类,能满足大多数场景,但如果能够了解AQS,对于理解这些工具类的原理非常有帮助,进而能够更好地使用它们。java.util.concurrent中基于AQS构建的阻塞类有:ReentrantLock、Semaphore、ReentrantReadWriteLock、CountDownLatch、SynchronousQueue、FutureTask等。
在基于AQS构建的同步容器类中,最基本的操作包括各种形式的获取和释放操作:
- “获取”操作是一种依赖于同步器状态的操作,通常会阻塞。当使用锁或信号量时,“获取”操作的含义很直观,即获取的是锁或者许可,并且调用者可能会一直等待,直到同步器类处于可被获取的状态。在CountDownLatch中,“获取”操作意味着“等待并直到闭锁到达结束状态”,而在使用“FutureTask”时,“获取”操作意味着“等待并直到任务已经完成的状态”。
- “释放”并不是一个可阻塞的操作,当执行“释放”操作时,所有在请求时被阻塞的线程都会开始执行。
从“获取”操作的定义可知,线程是否会阻塞取决于“某个状态”,AQS负责管理同步器类中的状态,它管理了一个整数状态信息,可以通过getState、setState、compareAndSetState等protected类型方法来进行操作。这个整数可以用来表示任意状态,与实际的业务有关。例如,ReentrantLock用它来表示所有者线程已经重复获取该锁的次数,Semaphore用它来表示剩余的许可数量,FutureTask用它来表示任务的状态(尚未开始、正在运行、已完成、已取消)。在同步容器类中还可以自行管理一些状态变量,例如,ReentrantLock保存了锁当前所有者信息,这样就能区分某个获取操作是重入的,还是竞争的。
如下伪代码给出了AQS中的获取操作与释放操作的形式。根据具体实现的同步类不同,“获取”操作可以是一种独占操作(例如ReentrantLock),也可以是一个非独占操作(例如Semaphore和CountDownLatch)。一个“获取”操作包含两部分,首先判断当前状态是否允许获取操作,如果是,则允许线程执行,否则将会阻塞请求或返回失败。
这种状态的判断是由同步器的语义决定的(即前文提到的state这个整数的语义与实际业务有关),例如,对于锁来说,如果它没有被某个线程持有,那么就能被成功地获取,而对于闭锁来说,如果它处于结束状态,那么也能被成功的获取。
boolean acquire() throws InterruptedException {
while (当前同步器的状态不允许获取操作) {
if (需要阻塞获取请求) {
如果当前线程不在同步队列中,则将其插入同步队列
阻塞当前线程.... // LockSupport.park
} else {
返回失败
}
}
可能更新同步器的状态
如果线程位于同步队列中,将其移出
返回成功
}
void release() {
更新同步器的状态
if(新的状态允许某个被阻塞的线程获取成功) {
解除队列中一个或多个线程的阻塞状态
}
}
AQS实现原理概述
第一种场景:有多个线程并发执行某个任务,为了保证线程安全,同一时刻只能有一个线程能执行临界区的代码,访问临界区的变量,这个时候我们可以使用对象锁的方式(synchronized)来实现,但是,这些线程究竟哪一个能竞争得到这把锁,是随机的。
所以,AQS解决的第一个问题就是让这些同步竞争锁的线程变得有序,先到先得。
现在有第二种场景,多线程并发执行某任务,还想要灵活的控制这些线程什么时间去竞争锁,什么时间不去竞争锁,可以灵活的赋予某个线程竞争的资格。在以前,可以通过object.wait()/object.notify()进行管理,但是object对象的这些方法没办法做到多条件多场景下的等待-通知,唤醒时具体唤醒的哪个线程也是随机的,难以满足复杂的业务场景。
所以,AQS解决的第二个问题就是更加灵活的控制线程的等待问题,同时引入多个等待队列。
为了解决第一个问题,AQS使用CLH队列作为同步队列,而不是简单地加一把锁就交给JVM来决定谁能获取锁,它是严格的FIFO队列。如下图所示。AQS为一系列同步器依赖于一个单独的原子变量(state)的同步器提供了一个非常有用的基础,获取到资源state后,线程便可执行临界区的代码了。

备注:同步队列的最佳选择是:那些自身没有使用底层锁来构造的非阻塞数据结构,业界主要有两种选择,一种是MCS锁,另一种是CLH锁。其中CLH一般用于自旋,但是相比MCS,CLH更容易实现取消和超时,所以同步队列选择了CLH作为实现的基础。
AQS不仅仅只有CLH同步队列(只能有一个),它还可以包含多个等待队列,它用于线程独占模式,不适用于线程共享资源的模式。等待队列的意思就是,等会再去竞争资源。在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列,这是AQS的优势之一:灵活。

Node节点
Node是一个包含了线程与线程状态的AQS的内部类,它定义了CLH队列(同步队列)以及Condition队列(等待队列)的节点信息。源码如下:
static final class Node {
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/** waitStatus values */
static final int CANCELLED = 1; // 线程由于超时或inturrepted被取消
static final int SIGNAL = -1; // 线程被唤醒后需要通知后续节点
static final int CONDITION = -2; // 线程处于condition等待队列,无法获取锁
static final int PROPAGATE = -3; // 共享模式下的SIGNAL
volatile int waitStatus;
volatile Node prev; // 前置节点
volatile Node next; // 后置节点
volatile Thread thread;
Node nextWaiter;
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;
}
}
Node节点的结构如下图:

prev、next:指向前置节点与后置节点,用于同步队列
thread: 当前线程对象
nextwaiter:用于资源共享模式,指向同步队列中资源共享的下一个节点;也用于等待队列的下一个节点
waitStatus: 当前节点的状态,是int类型,有以下取值
| 值 | 标识 | 含义 |
|---|---|---|
| 1 | CANCELL | 当前节点由于超时或中断被取消,节点进入该状态后保持不变 |
| 0 | 无 | 节点初始状态 |
| -1 | SIGNAL | 当前节点的后继节点被阻塞,因此当前节点在释放或者取消的时候需要唤醒它的后继节点 |
| -2 | CONDITION | 当前节点处于等待队列中,不能获取锁,只有同步队列的节点才能获取锁 |
| -3 | PROPAGATE | 当前节点获取到锁的信息,需要传递给后继节点,用于资源共享模式 |
队列中的Node
同步队列使用pre、next实现双向链表,等待队列使用nextWaiter实现单向链表。
同步队列独占模式
一个节点独占一把锁。

同步队列共享模式
多个节点可以共享一把锁,下图中nextWaiter指向了那些被共享的节点。

等待队列中的Node
单向链表,这些节点没有获取锁的权限,它们的waitStatus都是Node.CONDITION

CLH同步队列
有两个成员变量:head、tail,指同步队列的首尾节点。
/**
* Head of the wait queue, lazily initialized. Except for
* initialization, it is modified only via method setHead. Note:
* If head exists, its waitStatus is guaranteed not to be
* CANCELLED.
*/
private transient volatile Node head;
/**
* Tail of the wait queue, lazily initialized. Modified only via
* method enq to add new wait node.
*/
private transient volatile Node tail;
入队
CLH队列是FIFO队列,故新的节点到来的时候,是要插入到当前队列的尾节点之后。
当一个线程成功地获取了同步状态,其他线程将无法获取到同步状态,转而其他线程被构造成为节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全,因此同步器提供了一个CAS方法,它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。
源码如下:入队时先判断队列是不是空的,如果是空的,进行初始化,然后再CAS插入链表尾部。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize,同步队列是一定有一个头节点的
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
出队
因为遵循FIFO规则,所以能成功获取到AQS同步状态的必定是首节点,首节点的线程在释放同步状态时,会唤醒后续节点,而后续节点会在获取AQS同步状态成功的时候将自己设置为首节点。
设置首节点是由获取同步成功的线程来完成的。由于只能有一个线程可以获取到同步状态,所以设置首节点的方法不需要像入队这样的CAS操作,只需要将首节点设置为原首节点的后续节点同时断开原节点、后续节点的引用即可。
源码如下:出队时,只需要将头节点进行更新即可。
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
Condition等待队列
AQS使用Condition实现等待队列(一个AQS只能有一个同步队列,但可以有多个等待队列)。等待队列只适用于线程独占资源模式,不适用于线程共享资源模式。
等待队列也是FIFO队列,队列中每个节点都包含了一个线程引用,它的节点类与同步队列的节点类都是Node类。
简介
Condition是一个接口,用来协调多线程间的通信,使得某个或某些线程一起等待某个条件,当满足该条件后(signal、signalAll方法被调用),等待的线程被唤醒,重新争夺锁。从定义来看,Condition与synchronize的wait()和notify()/notifAll()的机制很相似。但是Conditon可以实现多路通知和选择性通知,灵活性更高。

当使用notify()/notifAll()时,JVM时随机通知线程的,具有很大的不可控性,所以java并发库多处使用Condition取代object的wait、notify。目前,Condition需配合lock才能使用(本质上是AQS实现了Condition接口,所以也可以配合AQS使用Condition,但是AQS太底层了,很少有程序员会直接使用AQS,一般都是通过并发工具类间接使用的)。如下图,Condition只在AQS和AQLS两个类中有实现(AQLS就是把AQS的state由整型改为长整型)。

使用示例如下:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
实现
当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中,当调用singal、singalAll方法时,将会唤醒在等待队列中等待时间最长的节点,移动至同步队列。
condition的部分源码如下,它是AQS的内部类,包含了等待队列的头节点、尾节点,只有一个无参构造器,await方法用来将
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
// 等待队列的头节点
private transient Node firstWaiter;
// 等待队列的尾节点
private transient Node lastWaiter;
public ConditionObject() {}
public final void await() throws InterruptedException {
// 省略,后文详细说明
}
public final void signal() {
// 省略,后文详细说明
}
public final void signalAll() {
// 省略
}
}
如下图:当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中,当调用singal、singalAll方法时,将会唤醒在等待队列中等待时间最长的节点。
入队
Condition拥有首尾节点的引用,新增节点只需要将原来的尾节点nextWaiter指向它,并且更新尾节点即可。与同步队列不同, 等待队列的入队过程不需要CAS保证,原因在于调用condition.await()方法的线程必定是获取了锁的线程。

源码如下:
// 当前线程包装为Node节点加入等待队列
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
// 节点包含了Thread信息Thread.currentThread()、节点状态Node.CONDITION
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
出队

出队就是把节点从等待队列移动至同步队列,源码如下:
注:transferForSignal()方法是AQS的方法,不是AQS的内部类ConditionObject的方法。
/**
* Transfers a node from a condition queue onto sync queue.
* Returns true if successful.
* @param node the node
* @return true if successfully transferred (else the node was
* cancelled before signal)
*/
final boolean transferForSignal(Node node) {
// CAS修改节点状态为0,初始态,这里如果修改失败只有一种可能就是该节点被取消
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 该节点加入到同步队列中去,详情参考同步队列入队
Node p = enq(node);
int ws = p.waitStatus; // CAS前置节点状态的预期值
// 如果前置节点被取消(ws>0)或修改前置状态为Node.SIGNAL失败,唤醒当前节点。
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
await()
Condition接口定义了await方法,AQS内部类ConditionObject实现了这个方法,主要有以下几步:
- 将当前线程包装为Node节点,加入条件队列
- 释放当前线程持有的同步锁
- 挂起线程
- 线程的挂起结束了(被其他线程唤醒),说明该Node节点出现在了同步队列中,开始自旋获取同步锁
- 扫尾工作,对于取消、中段的处理
// condition的await方法
public final void await() throws InterruptedException {
if (Thread.interrupted()) // 检测线程中断状态
throw new InterruptedException();
Node node = addConditionWaiter(); // 将当前线程包装为Node节点加入条件队列
int savedState = fullyRelease(node); // 先释放节点线程占用的同步锁
int interruptMode = 0;
// 判断当前线程对应的节点是不是在同步队列,如果不是,就挂起,使用while循环自旋而不是if语句的原因是,避免LockSupport.unpark被调用导致该线程节点明明仍在条件队列中,却去竞争锁资源。
while (!isOnSyncQueue(node)) { // 代码段1
LockSupport.park(this); // 挂起
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 挂起结束了,线程出现在了同步队列中,开始尝试获取锁
// 代码段2,自旋获取锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 扫尾,清除位于取消状态的节点
if (node.nextWaiter != null)
unlinkCancelledWaiters();
// 扫尾,如果前面有任何过程被中断,最后再抛出中断异常
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
// 代码段1,判断该节点是否为同步队列的节点
final boolean isOnSyncQueue(Node node) {
// 同步队列是有头节点的,而条件队列没有
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
// next字段只有同步队列才会使用,条件队列中使用的是nextWaiter字段
if (node.next != null)
return true;
//上面如果无法判断则进入复杂判断
return findNodeFromTail(node); // 从尾节点一个个往前扫描,看看同步队列是否存在该节点
}
// 代码段2,同步队列中的节点不断地自旋,不断地尝试获取锁。返回true表示被中断过
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); // 没有获取到锁被中断了,取消获取锁
}
}
signal()
await方法中,会将线程节点加入到等待队列,然后再挂起线程,线程是这样被被挂起的:
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
要想把线程唤醒,就需要执行LockSupport.unpark,同时还要满足isOnSyncQueue(节点在同步队列)。
但是singal不是唤醒线程,只是把线程从等待队列移动至同步队列,至于线程何时被唤醒(获取到锁资源),要看资源何时被释放。
//条件队列唤醒入口
public final void signal() {
//如果不是独占锁则抛出异常,再次说明条件队列只适用于独占锁
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//如果条件队列不为空,则进行唤醒操作
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null) // 查找下一个节点
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&(first = firstWaiter) != null);//将节点加入同步队列,如果失败则会查找条件队列上等待的下一个节点直到队列为空
}
线程如何被唤醒
上面提到了,signal只负责把线程移动至同步队列,此时线程还没被唤醒,必须得调用LockSupport.unpark才能唤醒线程,在整个AQS类中,unpark是在unparkSuccessor中执行的,因为能够执行unparkSuccessor的线程一定是持有锁的线程,所以该方法用来unpark同步队列中下一个节点对应的线程。
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
// 首先更新waitStatus,改为初始值,因为node.next节点对应的线程即将被释放了。
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
// 如果下个节点是空的(传进来的这个线程)或者处于被取消的状态,从同步队列尾部遍历链表,找到第一个没被取消的节点(waitStatus<=0),释放该节点,在尾部遍历的情况下,成了先进后出队列了,至于为什么这么做,可能考虑的是node.next==null或者s.waitStatus>0属于异常情况,对于异常情况采用异常处理。
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);
}
unparkSuccessor是一个私有的方法,只有三处调用了它,release、doReleaseShared、 cancelAcquire,它们分别是:释放独占锁、释放共享锁、取消竞争锁。只看release就可以。
public final boolean release(int arg) {
if (tryRelease(arg)) { // tryRelease是抽象方法,这个方法需要同步工具类自己实现,AQS不再管了
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
release只有一个同步工具类提供公有方法可以调用:ReentrantLock的unlock方法。
public void unlock() {
sync.release(1);
}
condition小结
总之,ConditionObject只是一个队列,它不负责何时调用release何时释放锁,它只负责把下个线程从等待队列移动到同步队列,至于await()方法何时才会跳出while (!isOnSyncQueue(node))自循环,完全交给同步工具类去控制。
这种设计非常灵活,你可以加入多个condition,然后再调用notify,只要锁资源被释放,位于同步队列的线程就有机会获取锁。目前来看,在所有的并发工具类中,Condition只能配合ReentrantLock才可以使用,除非你在自定义的同步工具类中继承AQS类,然后再调用release方法。
AQS是如何阻塞线程的
基于LockSupport实现线程的阻塞管理,LockSupport.park阻塞当前线程直到有1个LockSupport.unpark方法被调用
public static void park() {
UNSAFE.park(false, 0L);
}
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
LockSupport实现原理简介
LockSupport具体的实现在java native方法,底层是有一个计数器的机制;每个java线程都有一个Parker实例,在Parker类里的_counter字段,就是用来记录线程的“许可”的。_counter大于0,表示有许可,否则,没有许可。
当调用park时,先判断counter是否大于0,若是,则把_counter设置为0并返回,否则,等待counter大于0或超时为止。
当调用unpark时,直接将counter设置为1,唤醒park中等待的线程。
在下面的例子中,永远也无法打印出“unpark…”,因为当前线程是无法执行到第3行,除非有其他线程能够调用LockSupport.unpark(t);
1 Thread t = Thread.currentThread();
2 LockSupport.park();
3 LockSupport.unpark(t);
4 System.out.println("unpark...");
unpark的调用是没有被计数的,在一个park调用前多次调用unpark方法,_counter字段的值只能是1,所以,只能解除一个park操作(因为调用park的线程被唤醒后会再次将counter设置为0)
示例2.6.1:
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread1 begin..");
LockSupport.park();
System.out.println("park1 release");
LockSupport.park();
System.out.println("park2 release");
LockSupport.park();
System.out.println("park3 release");
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread2 begin..");
LockSupport.unpark(thread1);
System.out.println("unpark1..");
LockSupport.unpark(thread1);
System.out.println("unpark2..");
LockSupport.unpark(thread1);
System.out.println("unpark3..");
}
});
thread1.start();
thread2.start();
}
上述代码执行结果如下,多次提前调用unpark,只解锁1个park。
thread2 begin..
unpark1..
unpark2..
unpark3..
thread1 begin..
park1 release
验证另外一种情况:多次调用park,再调用unpark能解锁几个park?
示例2.6.2与执行结果如下,让thread1先执行3次park,然后在thread2中执行1次unpark,从控制台打印来看,只解锁一个park。
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread1 begin..");
LockSupport.park();
System.out.println("park1 release");
LockSupport.park();
System.out.println("park2 release");
LockSupport.park();
System.out.println("park3 release");
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread2 begin..");
LockSupport.unpark(thread1);
System.out.println("unpark1..");
}
});
thread1.start();
thread2.start();
}
thread1 begin..
thread2 begin..
unpark1..
park1 release
LockSupportd的优势
优势是:park和unpark的调用没有时序的限制。
unpark函数可以先于park调用。比如线程B调用unpark函数,给线程A发了一个“许可”,那么当线程A调用park时,它发现已经有“许可”了,那么它会马上再继续运行,一个线程它有可能在别的线程unPark之前,或者之后,或者同时调用了park,那么因为park的特性,它可以不用担心自己的park的时序问题,否则,如果park必须要在unpark之前,那么给编程带来很大的麻烦。示例可参考上节示例2.6.1。
建议不要再使用object的wait-notify机制,全部使用LockSupport实现wait-notify机制,二者对比如下:
LockSupportd小结
调用park时,如果count<=0,会等待count大于0或超时,如果count>0,会将count设置为0并返回;调用unpark时,将count设置为1。因此,有以下三个特点:
- 没有时序的限制,可以先执行unpark再执行park
- 多次调用park,再调用1次unpark,只能解锁1个park
- 多次调用unpark,再多次调用park,只能解锁1个park
park、unpark与wait、notify的区别
目前来看,park、unpark只有优势,还没看到缺点在哪里。
LockSupport比Object的wait/notify优势:
- LockSupport不需要在同步代码块里 ,所以线程间也不需要维护一个共享的同步对象了,实现了线程间的解耦;而wait/notify必须在同步块或同步方法中才能调用。
- unpark函数可以先于park调用,所以不需要担心线程间的执行的先后顺序,而wait必须先于notify。
// 示例1 使用wait/notify实现生产者、消费者模式
public class Test {
public static void main(String args[]) {
final Object lock1 = new Object();
Thread producer = new Thread(new Runnable() {
@Override
public void run() {
for (int i=0; i < 100; i ++) {
try {
Thread.sleep(300L);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
lock1.notifyAll();
System.out.println("生产者..");
try {
lock1.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
Thread consumer = new Thread(new Runnable() {
@Override
public void run() {
for (int i=0; i < 100; i ++) {
try {
Thread.sleep(300L);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
lock1.notifyAll();
System.out.println("消费者..");
if (i % 5 == 0) {
try {
lock1.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
});
producer.start();
consumer.start();
}
}
// 示例2 使用park、unpark实现生产者、消费者模式
public class Test {
public static void main(String args[]) {
Thread producer = new Thread(new Runnable() {
@Override
public void run() {
for (int i=0; i < 100; i ++) {
try {
Thread.sleep(300L);
} catch (InterruptedException e) {
e.printStackTrace();
}
LockSupport.park();
System.out.println("生产者..");
}
}
});
Thread consumer = new Thread(new Runnable() {
@Override
public void run() {
for (int i=0; i < 100; i ++) {
try {
Thread.sleep(300L);
} catch (InterruptedException e) {
e.printStackTrace();
}
LockSupport.unpark(producer);
System.out.println("消费者..");
}
}
});
producer.start();
consumer.start();
}
}
资源共享方式
AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。独占与共享最大不同就在各自的获取锁时,对于独占来说只有true或false,只有一个线程得以执行任务;而对于共享锁的tryAcquireShared来说,线程数没达到限制都可以直接执行。
- 独占方式,每次只允许一个线程执行,当前线程执行完会release将同步状态归零,再唤醒后继节点,这里通过自定义tryAcquire来实现公平与非公平(即是否允许插队)
- 共享方式,允许一定数目线程执行,比如Semaphore,当前正在执行的线程数小于限制值state,就CAS更改同步状态值,线程直接执行。CountDownLatch也是使用的这种模式。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下某几种方法:
- isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}
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();
}
下面的这些方法分别是AQS已封装好的供同步工具类调用的获取-释放资源的方法,里面关于独占模式涉及到的方法,在介绍Condition时已经介绍过了,关于共享模式的具体实现,原理类似不介绍了。
/*
* 独占模式的两个接口,供外部调用
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // acqire就是把节点加入到同步队列
selfInterrupt();
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // release就是unpark同步队列的某一个线程
return true;
}
return false;
}
/*
* 共享模式的两个接口,供外部调用
*/
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
同步工具类
同步工具类可以是任何一个对象,只要它满足:可以根据自身的状态来协调线程控制流。
同步容器中的阻塞队列是一种同步工具类,它们不仅能作为保存对象的容器,还能通过take、put这两个阻塞操作协调生产者、消费者等线程之间的控制流。
其他类型的同步工具类有:信号量Semaphore、栅栏Barrier、闭锁Latch。如果这些类不能满足需要,可以根据同步工具类的定义创建所需的同步工具类。
同步工具类具有下面的特性:
- 封装了某些状态,这些状态决定执行同步工具类的线程是继续执行还是等待
- 提供对上述状态的操作
闭锁CountDownLatch
闭锁是一种可以延迟线程的进度直至达到其终止状态的同步工具类。将闭锁理解为一扇门,在闭锁到达结束状态之前,这扇门一直是关闭的,等到达结束状态时,这扇门会打开允许所有线程通过,此后,闭锁的状态不会再改变,这扇门永远保持打开的状态。
应用场景
闭锁可以应用于以下场景:
- 模拟瞬时高并发
- 某个服务在其依赖的其他服务都启动后才启动
- 多玩家游戏中,游戏的所有玩家都就绪后才进入游戏
CountDownLatch原理
CountDownLatch是一种常见的闭锁,它包含了一个递减计数器,在CountDownLatch初始化时指定计数器的初始值,表示要等待的事件数量,它的await方法指定了闭锁的位置,直到计数器值为0时,才执行await方法后面的代码。
CountDownLatch是基于AQS的共享方式实现的。使用AQS的state变量来存放计数器的值。在调用CountDownLatch的构造函数时,会调用内部类Sync的构造函数将值赋给state变量,当多个线程调用countdown方法时实际是使用CAS递减state变量的值;当线程调用await方法后当前线程会被放入AQS阻塞队列等待计数器为0时返回,即所有线程都调用了countdown方法时。最后,当计数器的值变为0时,当前线程还会调用AQS的doReleasedShared()方法激活所有因调用await()方法而被阻塞的线程。
public class CountDownLatch {
//实现AQS组件的内部类,可以看出CountDownLatch是通过AQS实现的
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
//设置计数器实际上是将值赋给了AQS状态变量state
Sync(int count) {
setState(count);
}
//获取状态变量state的值
int getCount() {
return getState();
}
// 重写父类的方法,参数acquires可以代表任何意思,比如一次加上多个共享锁,在countDownLatch中没有使用acquires参数
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
// 重写父类的方法,参数releases可以代表任何意思,比如一次释放多个共享锁,在countDownLatch中没有使用该参数
protected boolean tryReleaseShared(int releases) {
//循环进行CAS,直到当前线程完成CAS减去1操作
for (;;) {
int c = getState();
//当前状态值为0则直接返回
if (c == 0)
return false;
int nextc = c-1;
//使用CAS让计数器值减去1
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
private final Sync sync;
//构造方法调用Sync类的构造函数
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//调用该方法后线程会被阻塞,直到发生下列情况之一才会返回:
// 1所有线程调用countdown方法,计数器的值变为0
// 2设置的时间到了,超时返回
// 3其他线程调用了当前线程的interrupt()方法中断了当前线程
public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout)); // 调用Sync的父类方法,最终调用了Sync类重写的tryAcquireShared
}
public void countDown() {
sync.releaseShared(1); // 调用Sync的父类方法,最终调用Sync重写的tryReleaseShared
}
//调用该方法获得state的值,一般在测试的时候使用
public long getCount() {
return sync.getCount();
}
}
// Sync的父类AbstractQueuedSynchronizer相关方法
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
implements java.io.Serializable {
// 获取锁
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquireShared(arg) >= 0 || // 子类Sync实现了tryAcquireShared
doAcquireSharedNanos(arg, nanosTimeout);
}
// 释放锁
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { // 子类Sync实现了tryReleaseShared
doReleaseShared(); // 在CountDownLatch中,状态值为0才会执行doReleaseShared
return true;
}
return false;
}
}
CountDownLatch使用
final CountDownLatch startGate = new CountDownLatch(1);
final CountDownLatch endGate = new CountDownLatch(nThread);
for(int i=0; i < nThread; i++) {
Thread t = new Thread () {
public void run() {
try {
startGate.await();
try {
task.run();
} finally {
endGate.countDown();
}
} catch (InteruptedException e) {
e.printStackTrace();
}
}
};
t.start();
}
long start = System.nanoTime();
startGate.countDownLatch();
endGate.await();
long end = System.nanoTime();
System.out.println("cost time" + end-start);
闭锁startGate:使得主线程能够同时启动所有的工作线程;
闭锁endGate:使得主线程等待最后一个线程执行完成计算耗时。
FutureTask
FutureTask也可以用作闭锁,FutureTask的计算通过Callable实现的,相当于一种可生成结果的Runnable,并且可以处于以下3种状态:等待运行、正在运行、完成运行。“完成运行”表示闭锁的门,当FutureTask进入该状态后,它会永远停在该状态。”完成运行“包含了所有可能的结束方式,正常结束、取消任务导致结束、异常导致结束。
futureTask.get()
用于获取任务的执行状态:
- 如果任务已经完成,则立刻返回结果
- 如果任务未完成,该方法将会阻塞,直至任务进入完成状态,然后返回结果或抛出异常。
FutureTask会将所有的异常封装为ExecutionException,在get中抛出。
应用场景
- 异步任务
- 耗时较长的任务,比如示例中数据预加载
示例:
public class Preloader {
Callable<ProductInfo> loadTask = new Callable<ProductInfo>() {
@Override
public ProductInfo call() throws DataLoadException {
return loadProductInfo(); // 耗时
}
};
private final FutureTask<ProductInfo> future = new FutureTask<ProductInfo>(loadTask);
private final Thread thread = new Thread(future);
public void start() {
thread.start();
}
public ProductInfo get() throws DataLoadException, InterruptedException {
try {
return future.get();
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof DataLoadException) {
throw (DataLoadException) cause;
}else if(cause instanceof InterruptedException) {
throw (InterruptedException) cause;
}else if(cause instanceof Error) {
logger.error("Error load data");
}else {
throw new IllegalStateException("Not unchecked", cause);
}
}
}
}
信号量Semaphore
Semaphore中管理着一组虚拟的许可,在执行操作时需要首先获取许可(acquire方法),并且在使用以后释放许可(release方法)。如果获取许可时,没有剩余的许可,那么会阻塞至获取到许可为止,阻塞可以被打断或设置超时。
应用场景
- 资源池,比如线程池、数据库连接池等
- 有边界的容器
Semaphore可以用于实现资源池,在获取资源之前调用acquire方法,在释放资源后调用release方法。当资源池为空时(没有剩余许可),将会阻塞等待获取资源而不是直接返回获取失败,不过使用BlockingQueue是一种更简单的方式,这也是信号量不常用的原因。
同样的,Semaphore可以将任何一种容器变为有界阻塞容器,在添加元素前调用acquire方法,在删除元素后调用release方法。在下面的示例中,信号量的计数值会初始化为容器的最大值。
public class BoundedHashSet<T> {
private final Set<T> set;
private final Semaphore sem;
public BoundedHashSet(int bound) {
this.set = Collections.synchronizedSet(new HashSet<T>());
this.sem = new Semaphore(bound);
}
public boolean add(T o) throws InterruptedException {
sem.acquire();
boolean wasAdded = false;
try {
wasAdded = set.add(o);
return wasAdded;
}
final {
if (!wasAdded) sem.release();
}
}
public boolean remove(T o) {
boolean wasRemoved = set.remove(o);
if (wasRemoved) sem.release();
return wasRemoved;
}
}
栅栏CyclicBarrier
闭锁是一次性对象,一旦进入终止状态,就不能被重置。
栅栏:所有线程必需同时到达栅栏位置,才能继续执行。
二者区别:
- 闭锁用于等待事件,栅栏用于等待其他线程。
- 闭锁是一次性,栅栏可以反复通过。
- CountDownLatch基于AQS;CyclicBarrier基于重入锁和Condition。本质上都是依赖于volatile和CAS实现的
CyclicBarrier原理
CyclicBarrier源码基于ReentrantLock和Condition实现,内部类Generation保证了栅栏的反复使用,每次通过栅栏后均会重置栅栏,核心代码是doawait方法,该方法的核心思路是,根据还能阻塞的线程个数count,判断调用该方法的线程是否为最后一个需要通过栅栏的线程:
- 如果是最后一个线程,开闸并重置栅栏,返回count值0,表示成功开闸
- 如果不是最后一个线程,调用condition的await方法等待,直至最后一个线程导致开闸、或出现中断异常、或不是当前栅栏代。
package java.util.concurrent;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class CyclicBarrier {
// 内部类,表示栅栏的迭代,因为栅栏可以反复通过,每一次通过后,更新为下一代
private static class Generation {
boolean broken = false; // 栅栏尚未被突破
}
/** 基于ReentrantLock和Condition实现等待(await)和通知(signal) */
private final ReentrantLock lock = new ReentrantLock();
private final Condition trip = lock.newCondition();
/** 拦截的线程数量 */
private final int parties;
/* 当屏障撤销时,需要执行的屏障操作 */
private final Runnable barrierCommand;
/** 当前栅栏代,每次开闸后更新,实现重置功能 */
private Generation generation = new Generation();
// 还能阻塞的线程个数,每一代栅栏更新时,重置该值
private int count;
/**
* 重置栅栏
*/
private void nextGeneration() {
trip.signalAll(); // 调用condition的signalAll方法唤醒所有线程
// set up next generation
count = parties;
generation = new Generation();
}
/**
* 突破栅栏时调用,设置栅栏代的状态,重置还能阻塞的线程个数,唤醒所有线程
*/
private void breakBarrier() {
generation.broken = true;
count = parties;
trip.signalAll(); // 调用condition的signalAll方法唤醒所有线程
}
/**
* 核心代码
*/
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock(); // 获取锁
try {
final Generation g = generation; // 保存此时的栅栏代
if (g.broken)
throw new BrokenBarrierException();
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
int index = --count; //还能阻塞的线程个数递减,然后作为本方法的返回值
//count个数为0,说明当前线程是最后一个到达的线程,开闸并重置栅栏,返回count
if (index == 0) {
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
// 若当前线程不是最后一个到达的线程
for (;;) {
try {
if (!timed)
trip.await(); // 调用condition的await方法等待,该方法会释放锁
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
// We're about to finish waiting even if we had not
// been interrupted, so this interrupt is deemed to
// "belong" to subsequent execution.
Thread.currentThread().interrupt();
}
}
if (g.broken)
throw new BrokenBarrierException();
if (g != generation)
return index;
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock(); // 确保一定释放了锁,不然其他线程执行该方法时会阻塞
}
}
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
public CyclicBarrier(int parties) {
this(parties, null);
}
public int getParties() {
return parties;
}
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
public boolean isBroken() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return generation.broken;
} finally {
lock.unlock();
}
}
public void reset() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
breakBarrier(); // break the current generation
nextGeneration(); // start a new generation
} finally {
lock.unlock();
}
}
public int getNumberWaiting() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return parties - count;
} finally {
lock.unlock();
}
}
}
cyclicBarrier使用
cyclicBarrier使用场景:将大问题划分为若干小问题,然后取小问题的结果整合为大问题的结果,类似于ReduceMap的思想。
class Solver {
final int N; //矩阵的行数
final float[][] data; //要处理的矩阵
final CyclicBarrier barrier; //循环屏障
class Worker implements Runnable {
int myRow;
Worker(int row) { myRow = row; }
public void run() {
while (!done()) {
processRow(myRow); //每个线程处理指定一行数据
try {
barrier.await(); //在屏障处等待直到
} catch (InterruptedException ex) {
return;
} catch (BrokenBarrierException ex) {
return;
}
}
}
}
public Solver(float[][] matrix) {
data = matrix;
N = matrix.length;
//初始化CyclicBarrier
barrier = new CyclicBarrier(N, new Runnable() {
public void run() {
mergeRows(...);//所有线程都到达栅栏时,执行一次该操作,把每个线程处理数据结果合并起来
}
});
for (int i = 0; i < N; ++i)
new Thread(new Worker(i)).start();
waitUntilDone();
}
}
BlockingQueue
BlockingQueue是一个阻塞队列接口,BlockingQueue基于队列容器提供了插入和取出数据的阻塞:
- 如果队列中没有数据时,取出数据的操作被阻塞直到队列中有数据能被取出
- 果队列中数据已经填满,则插入数据的操作将被阻塞,直到队列中有空位能够插入

BlockingQueue接口定义了下列方法,根据失败时的表现,可分为:
- 失败抛出异常: 若操作无法立即执行,抛出异常
- 失败返回特殊值:若操作无法立即执行,返回特定值
- 失败则阻塞:若操作无法立即执行,该方法调用将会发生阻塞,直至能够执行
- 失败则阻塞至超时:若操作无法立即执行,该方法调用将会发生阻塞,若在超时时间内还不能执行,则返回特殊值以告知该方法是否执行成功
| 失败抛出异常 | 失败返回特殊值 | 失败则阻塞 | 失败则阻塞至超时 | |
|---|---|---|---|---|
| 插入 | add() | offer(e) | put() | offer(e, time, timeUnit) |
| 取出(查询并删除) | remove() | poll() | take() | poll(time, timeUnit) |
| 查询 | element() | peek() | / | / |
使用BlockingQueue的场景多为阻塞,所以最常用的方法是: put、take、offer(e, time, timeUnit)、poll(time, timeUnit)
BlockingQueue的实现靠的是AQS的Condition提供的await和signal。
BlockingQueue实现类主要有7类:
- ArrayBlockingQueue,数组阻塞队列,是有界队列。
- LinkedBlockingQueue,链表阻塞队列,链表的上限是Integer.MAX_VALUE,可以自定义上限但没必要(自定义上限的话直接使用ArrayBlockingQueue了),可视作无界阻塞队列。
- DelayQueue,延时阻塞队列,在阻塞队列的基础上,又加入了延时的要求,比如元素添加一定时延后,才允许从队列中取出,是无界队列。
- PriorityBlockingQueue,带有优先级的无界阻塞队列
- SynchronousQueue,同步阻塞队列,它的内部同时只能够容纳单个元素。
- BlockingDeque,阻塞双端队列
- LinkedBlockingDeque, 基于链表的阻塞双端队列
有关这七种队列的实现参考:JAVA并发(一)任务执行框架Executor
本文详细介绍了JAVA并发编程中的AQS(AbstractQueuedSynchronizer)原理及其在同步工具类中的应用,如CountDownLatch、Semaphore、CyclicBarrier和BlockingQueue等。通过分析AQS的状态管理、CLH同步队列、Node节点以及Condition等待队列,揭示了线程同步和等待的实现机制。此外,文章还对比了LockSupport与wait/notify机制,以及讨论了多种资源共享方式,展示了如何利用这些工具类实现高效的并发控制。
740

被折叠的 条评论
为什么被折叠?



