构造线程安全类时常用的一个策略是将线程安全委托给现有的线程安全类,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