1.什么是AQS?
AQS是AbstractQueueSynchronizer是缩写,其意思就是抽象队列同步器。原理就是通过一个FIFO队列维护一个state同步状态值,只需要继承该抽象类重写对应的方法便可实现一套线程同步机制。
AQS抽象类在java.util.concurrent.locks包下定义。
如上图所示,AQS属于一个抽象类,主要是用来构建锁结构以及同步器的一个抽象类。AQS继承了AbstractOwnableSynchronizer,其作用为设置和获取独占锁的拥有者线程方法。实现Serializable类是可进行序列化和反序列化作用。
AQS 为构建锁和同步器提供了一些通用功能的是实现,因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask(jdk1.7) 等等皆是基于 AQS 的。
2.AQS原理
2.1.AQS原理概述
如何来描述AQS的工作原理链路呢?核心思想理解就是:AQS主要是维护state同步状态值来实现同步机制,根据state的值来实现线程状态设置。如果被请求的共享资源空闲,则将当前请求资源的工作线程设置为有效线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒的一套机制进行锁分配。AQS就可以通过CLH队列实现阻塞唤醒机制,将暂时获取不到锁的线程加入到队列中。
CLH队列: CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。
AQS是维护一个int类型的state共享资源表示当前同步状态,同时内置一个FIFO队列实现获取共享资源的排队工作。AQS是使用CAS操作来实现共享资源state变更的原子操作。
AQS抽象类定义了保护类型的获取state资源以及设置同步状态的值方法。主要是通过getState(),setState(),compareAndSetState() 进行操作state值变更。
// 返回同步状态的当前值
protected final int getState() {
return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
// 原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
2.2.AQS对资源共享方式
AQS实现资源的共享方式定义了两种
- Exclusive(独占):独占说明就是同步锁,也可理解为写锁,就是只能允许一条线程获取共享资源,其他线程则需要阻塞在外部。例如ReentrantLock锁就为同步锁,又分为公平锁和非公平锁。
- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁。
- 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的。
- Share(共享):共享说明就是乐观锁,也可以理解为读锁,就是允许多条线程同时执行,常用于读多写少的场景。如 CountDownLatch、Semaphore、 CyclicBarrier、ReadWriteLock。
ReentrantReadWriteLock 类维护了读锁和写锁两种类型的锁,不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS 已经在顶层实现好了,子类只需要实现AQS使用模板方法定义的获取共享资源的方法就可以操作AQS资源。
下面我拿到了ReentrantLock类的实现方法源码
ReentrantLock 默认采用非公平锁,因为考虑获得更好的性能,通过 boolean 来决定是否用公平锁(传入 true 用公平锁)。
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
public ReentrantLock() {
// 默认非公平锁
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock 中公平锁的 lock 方法。
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
// AbstractQueuedSynchronizer.acquire(int arg)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 1. 和非公平锁相比,这里多了一个判断:是否有线程在等待
if (!hasQueuedPredecessors() &&
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;
}
}
非公平锁的 lock 方法:
static final class NonfairSync extends Sync {
final void lock() {
// 2. 和公平锁相比,这里会直接先进行一次CAS,成功就返回了
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// AbstractQueuedSynchronizer.acquire(int arg)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
/**
1. Performs non-fair tryLock. tryAcquire is implemented in
2. subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 这里没有对阻塞队列进行判断
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
公平锁和非公平锁也只有2处地方有不同的点,公平锁会进行入队排序尝试获取锁,而非公平锁入队之前会先尝试获得共享资源的更新权。
- 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
- 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
所以公平锁会遵循队列原则进行排队获取共享资源,而非公平锁会在每一次能够尝试获取共享资源时都进行尝试。
公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。(后续进入到阻塞队列也不需要和公平锁一样按照顺序尝试获取锁资源,所有的线程会进行抢占)
相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
因为非公平锁减少了线程挂起的几率,后来的线程有一定几率逃离被挂起的开销。上面两个不同点的第一个区别也是非公平锁性能提高的原因,避免阻塞队列中线程唤醒以及抢占资源所消耗的时长,提高性能。
ReentrantLock的unlock方法
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
// 如果不是自身锁对象调用unlock()方法的话,就报异常
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
// 如果标志位已经为0,表示重入锁已经全部释放,这将当前获取锁的线程设置为null,以便其他线程进行加锁
setExclusiveOwnerThread(null);
}
// 更新重入锁解锁到达的次数,如果C不为0,表示还有重入锁unlock()没有调用完
setState(c);
return free;
}
如果tryRelease()方法成功执行,表示之前获取锁的线程已经执行完所有需要同步的代码(重入锁也完全退出),那么就需要唤醒同步阻塞队列中的第一个等待的线程(也是等待最久的线程),执行unparkSuccessor(h)方法:
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// 先获取头结点(哨兵节点)的waitStatus状态,如果小于0,则可以获取锁,并将waitStatus的状态设置为0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
// 如果哨兵节点的下一个节点为null,或者状态为1表示已经取消,则依次循环寻找(从后往前寻找)后面节点,直至找到一个waitStatus<0的节点,并将该节点设置为需要获取锁的节点
s = null; // 注下面的 for 循环是从后往前遍历,直到 for 循环遍历完成找到最开头的一个节点,并且该节点 waitStatus<0。将找到的最开头的满足条件的节点给到 s 并对其进行唤醒
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 将该node节点的线程解锁,允许它去获取锁,然后执行业务逻辑
LockSupport.unpark(s.thread);
}
2.3.AQS模板方法
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):
- 使用者继承 AbstractQueuedSynchronizer 并重写指定的方法。(这些重写方法很简单,无非是对于共享资源 state 的获取和释放)
- 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。
需要实现一个同步器需要重写哪些AQS提供的钩子方法呢。
protected boolean tryAcquire(int)// 独占方式。尝试获取资源,成功则返回true,失败则返回false。
protected boolean tryRelease(int)// 独占方式。尝试释放资源,成功则返回true,失败则返回false。
protected int tryAcquireShared(int)// 共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
protected boolean tryReleaseShared(int)// 共享方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean isHeldExclusively()// 该线程是否正在独占资源。只有用到condition才需要去实现它
只需要实现独占锁/共享锁功能,可只实现tryAcquire/tryRelease 或者 tryAcquireShared/tryReleaseShared。 虽然实现tryAcquire/tryRelease 可自行设定逻辑,但建议使用state 方法对 state 变量进行操作 以实现同步类。上述提到的钩子方法是什么样的呢? 钩子方法是一种被声明在抽象类中的方法,它可以是空方法(由子类实现),也可以是默认实现的方法。模板设计模式通过钩子方法控制固定步骤的实现。
除了上述提到的钩子方法之外,AQS类中其他方法都是final修饰的方法,子类是不可以重写或者实现的。
下面就以几个实例来描述state共享资源争夺变更逻辑。
- ReentrantLock:state 初始化为 0,表示未锁定状态。A 线程 lock() 时,会调用 tryAcquire() 独占该锁并将 state+1 。此后,其他线程再 tryAcquire() 时就会失败,直到 A 线程 unlock() 到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多少次,这样才能保证 state 是能回到零态的。
- CountDownLatch:任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown() 一次,state 会 CAS(Compare and Swap) 减 1。等到所有子线程都执行完后(即 state=0 ),会 unpark() 主调用线程,然后主调用线程就会从 await() 函数返回,继续后余动作。
自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。
public class Mutex extends AbstractQueuedSynchronizer {
@Override
public boolean tryAcquire(int arg) {
return compareAndSetState(0, 1);
}
@Override
public boolean tryRelease(int arg) {
return compareAndSetState(1, 0);
}
public static void main(String[] args) {
final Mutex mutex = new Mutex();
new Thread(() -> {
System.out.println("thread1 acquire mutex");
mutex.acquire(1);
// 获取资源后sleep保持
try {
TimeUnit.SECONDS.sleep(5);
} catch(InterruptedException ignore) {
}
mutex.release(1);
System.out.println("thread1 release mutex");
}).start();
new Thread(() -> {
// 保证线程2在线程1启动后执行
try {
TimeUnit.SECONDS.sleep(1);
} catch(InterruptedException ignore) {
}
// 等待线程1 sleep结束释放资源
mutex.acquire(1);
System.out.println("thread2 acquire mutex");
mutex.release(1);
}).start()
}
}
上述示例代码是通过实现AQS抽象类实现互斥逻辑,线程1获取mutex后,线程2的acquire陷入阻塞,直到线程1释放。其中tryAcquire/acquire/tryRelease/release的arg参数可按实现逻辑自定义传入值,无具体要求。
2.4.AQS结构
上面提到了AQS是维护一个int类型的state共享资源,同时使用CLH队列来进行线程任务阻塞排队的,AQS就是将Node节点进行包装线程然后根据Node类实现CLH队列FIFO类型队列。
2.4.1.Node节点
前文提到,在AQS中如果线程获取资源失败,会包装成一个节点挂载到CLH队列上,AQS中定义了Node类用于包装线程。
Node主要包含5个核心字段:
- waitStatus:当前节点状态,该字段共有5种取值:
- CANCELLED = 1。节点引用线程由于等待超时或被打断时的状态。
- SIGNAL = -1。后继节点线程需要被唤醒时的当前节点状态。当队列中加入后继节点被挂起(block)时,其前驱节点会被设置为SIGNAL状态,表示该节点需要被唤醒。
- CONDITION = -2。当节点线程进入condition队列时的状态。(见ConditionObject)
- PROPAGATE = -3。仅在释放共享锁releaseShared时对头节点使用。(见共享锁分析)
- 0。节点初始化时的状态。
- prev:前驱节点。
- next:后继节点。
- thread:引用线程,头节点不包含线程。
- nextWaiter:condition条件队列。(见ConditionObject)
AQS的工作线程就是将线程封装成Node节点同时使用内部的prev以及next来实现队列。
2.4.2.独占锁分析
抢占锁资源首先需要先执行acquire方法获取state的变更权
public final void acquire(int arg) {
// tryAcquire需实现类处理
// 如获取资源成功,直接返回
if (!tryAcquire(arg) &&
// 如获取资源失败,将线程包装为Node添加到队列中阻塞等待
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 如阻塞线程被打断
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 跟非公平锁的实现相比,这里多了!hasQueuedPredecessors()的判断。Predecessor:前任,!hasQueuedPredecessors()顾名思义,没有排队着的前任(线程),则当前线程排第一
if (!hasQueuedPredecessors() &&
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;
}
acquire核心为tryAcquire、addWaiter和acquireQueued三个函数,其中tryAcquire需具体类实现。 每当线程调用acquire时都首先会调用tryAcquire,失败后才会挂载到队列,因此acquire实现默认为非公平锁。
addWaiter将线程包装为独占节点,尾插式加入到队列中,如队列为空,则会添加一个空的头节点。值得注意的是addWaiter中的enq方法,通过CAS+自旋的方式处理尾节点添加冲突。
acquireQueue在线程节点加入队列后判断是否可再次尝试获取资源,如不能获取则将其前驱节点标志为SIGNAL状态(表示其需要被unpark唤醒)后,则通过park进入阻塞状态。
acquireQueued方法核心逻辑为for(;;)和shouldParkAfterFailedAcquire。tail节点默认初始状态为0,当新节点被挂载到队列后,将其前驱即原tail节点状态设为SIGNAL,表示该节点需要被唤醒,返回true后即被park陷入阻塞。for循环直到节点前驱为head后才尝试进行资源获取。
- acquire方法尝试获取state共享资源变更权限
- tryAcquire方法尝试获取资源
- 获取state值,判断当前申请资源数是否能够被state值满足,如果能够满足说明可以获取共享资源的变更权限,如果不能够满足则需要响应false当前线程入阻塞队列。
- 如果当前state满足请求数则需要自旋CAS方法实现原子方式更改state共享资源的值并且设置当前线程为工作线程。
- addWaiter方法将线程封装成Node节点并且通过尾插法插入到CLH队列的尾部然后自旋处理尾节点
- 调用acquireQueued方法入队列
- 再次判断是否可尝试获取资源,调用tryAcquire方法尝试获取资源变更权限,尝试获取失败则调用shouldParkAfterFailedAcquire方法将当前新节点挂载到队列尾部。
- tryAcquire方法尝试获取资源
release方法逻辑
release流程较为简单,尝试释放成功后,即从头结点开始唤醒其后继节点,如后继节点被取消,则转为从尾部开始找阻塞的节点将其唤醒。阻塞节点被唤醒后,即进入acquireQueued中的for(;;)循环开始新一轮的资源竞争。
2.4.3.共享锁分析
public final void acquireShared(int arg) {
// 负数表示获取共享锁失败,不同于tryAcquire的bool返回
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
// doAcquireShared核心代码
final Node node = addWaiter(Node.SHARED);
...
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
// r>=0 表示获取锁成功,调整头结点并传递唤醒
setHeadAndPropagate(node, r);
}
}
...
}
acquireShared和releaseShared整体流程与独占锁类似,tryAcquireShared获取失败后以Node.SHARED挂载到队尾阻塞,直到队头节点将其唤醒。在doAcquireShared与独占锁不同的是,由于共享锁是可以被多个线程获取的,因此在首个阻塞节点被唤醒后,会通过setHeadAndPropagate传递唤醒后续的阻塞节点。
setHeadAndPropagate和doReleaseShared构成共享锁唤醒的核心逻辑。
进入setHeadAndPropagate,首先需要明确的是,该函数的传入参数propagate一定是非负数,接下来其唤醒主要为两个判断逻辑:
- 如果propagate > 0,表示存在多个共享锁可以获取,可直接进行doReleaseShared唤醒阻塞节点。
- 如果propagate = 0,表示仅当前节点可被唤醒,则有两种情况:
- h == null || h.waitStatus < 0,通常情况下h != null,现给出h.waitStatus < 0的场景。
2.4.4.ConditionObject
ConditionObject是AQS的一个子类,用于实现条件变量的功能,可以在获得锁后唤醒或等待条件。
AQS中Node除了组成阻塞队列外,还在ConditionObject中得到应用,ConditionObject的核心定义为:
public class ConditionObject implements Condition, java.io.Serializable {
...
private transient Node firstWaiter;
private transient Node lastWaiter;
...
}
ConditionObject通过Node也构成了一个FIFO的队列,那么ConditionObject为AQS提供了怎样的功能呢?
public interface Condition {
...
void await() throws InterruptedException;
void signal();
void signalAll();
...
}
每个条件变量都维护了一个容器,ConditionObject中的容器就是单向链表队列,上面的属性就是队列的头结点firstWaiter和尾结点lastWaiter,需要注意,条件队列中的头结点不是虚拟头结点,而是包装了等待线程的节点!其类型和同步队列一样,也是使用AQS的内部类Node来构成,但与同步队列不同的是,条件队列是一个单向链表,所以他并没有使用Node类中的next属性来关联后继Node,而使用的nextWaiter。
查看Condition接口的定义,可以看到其定义的方法与Object类的wait/notify/notifyAll功能是一致的。
volatile Node prev;
volatile Node next;
Node nextWaiter;
这里我们需要注意,nextWaiter是没用volatile修饰的,为什么呢?因为线程在调用await方法进入条件队列时,是已经拥有了锁的,此时是不存在竞争的情况,所以无需通过volatile和cas来保证线程安全。而进入同步队列的都是抢锁失败的,所以肯定是没有锁的,故要考虑线程安全。
ConditionObject机制如上图,在条件队列中,Node采用nextWaiter组成单向链表,当持有锁的线程发起condition.await调用后,会包装为Node挂载到Condition条件阻塞队列中;当对应condition.signal被触发后,条件阻塞队列中的节点将被唤醒并挂载到锁阻塞队列中。ConditionObject的队列逻辑与前述的acquire/release大同小异,不再赘述。
signAll方法
public final void signalAll() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignalAll(first);
}
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
// 将next指向first的后继Node
Node next = first.nextWaiter;
// 切断first与后继Node的联系
first.nextWaiter = null;
// 将此node转移到同步队列中
transferForSignal(first);
// 将first指向first的后继Node
first = next;
// 在判断此时的first是否为null,不是则继续循环
} while (first != null);
}
因为是移出条件队列中所有的Node,所以一开始我们通过将头结点和尾节点置为null来“清空”条件队列,然后通过do-while循环将条件队列中所有节点通过transferForSignal方法一个一个转移到同步队列中。
signal方法
signalAll是将条件队列中所有的Node转移到同步队列,signal则只转移条件队列中的第一个状态不为CANNCELLED的Node,直接看源码。
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
do {
// 将firstWaiter指向传入的first的后继节点,
// 然后判断firstWaiter是否为null,
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
与signalAll不同点在于doSignal方法需要先处理队列中第一个节点
3.Semaphore(信号量)
synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
示例代码如下:
/**
* @Description: 需要一次性拿一个许可的情况
*/
public class SemaphoreExample1 {
// 请求的数量
private static final int threadCount = 550;
public static void main(String[] args) throws InterruptedException {
// 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢)
ExecutorService threadPool = Executors.newFixedThreadPool(300);
// 一次只能允许执行的线程数量。
final Semaphore semaphore = new Semaphore(20);
for (int i = 0; i < threadCount; i++) {
final int threadnum = i;
threadPool.execute(() -> {// Lambda 表达式的运用
try {
semaphore.acquire();// 获取一个许可,所以可运行线程数量为20/1=20
test(threadnum);
semaphore.release();// 释放一个许可
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
threadPool.shutdown();
System.out.println("finish");
}
public static void test(int threadnum) throws InterruptedException {
Thread.sleep(1000);// 模拟请求的耗时操作
System.out.println("threadnum:" + threadnum);
Thread.sleep(1000);// 模拟请求的耗时操作
}
}
执行 acquire() 方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个 release 方法增加一个许可证,这可能会释放一个阻塞的 acquire() 方法。然而,其实并没有实际的许可证这个对象,Semaphore 只是维持了一个可获得许可证的数量。 Semaphore 经常用于限制获取某种资源的线程数量。
当然一次也可以一次拿取和释放多个许可,不过一般没有必要这样做:
semaphore.acquire(5);// 获取5个许可,所以可运行线程数量为20/5=4
test(threadnum);
semaphore.release(5);// 释放5个许可
除了 acquire() 方法之外,另一个比较常用的与之对应的方法是 tryAcquire() 方法,该方法如果获取不到许可就立即返回 false。
Semaphore 有两种模式,公平模式和非公平模式。
- 公平模式: 调用 acquire() 方法的顺序就是获取许可证的顺序,遵循 FIFO;
- 非公平模式: 抢占式的。
Semaphore 对应的两个构造方法如下:
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
这两个构造方法,都必须提供许可的数量,第二个构造方法可以指定是公平模式还是非公平模式,默认非公平模式。
Semaphore 与 CountDownLatch 一样,也是共享锁的一种实现。它默认构造 AQS 的 state 为 permits。当执行任务的线程数量超出 permits,那么多余的线程将会被放入阻塞队列 Park,并自旋判断 state 是否大于 0。只有当 state 大于 0 的时候,阻塞的线程才能继续执行,此时先前执行任务的线程继续执行 release() 方法,release() 方法使得 state 的变量会加 1,那么自旋的线程便会判断成功。 如此,每次只有最多不超过 permits 数量的线程能自旋成功,便限制了执行任务线程的数量。
4.CountDownLatch(倒计时器)
CountDownLatch 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。
CountDownLatch 是共享锁的一种实现,它默认构造 AQS 的 state 值为 count。当线程使用countDown() 方法时,其实使用了tryReleaseShared方法以 CAS 的操作来减少 state,直至 state 为 0 。当调用 await() 方法的时候,如果 state 不为 0,那就证明任务还没有执行完毕,await() 方法就会一直阻塞,也就是说 await() 方法之后的语句不会被执行。然后,CountDownLatch 会自旋 CAS 判断 state == 0,如果 state == 0 的话,就会释放所有等待的线程,await() 方法之后的语句得到执行。
4.1.CountDownLatch两种典型
- 某一线程在开始运行前等待 n 个线程执行完毕。
将 CountDownLatch 的计数器初始化为 n (new CountDownLatch(n)),每当一个任务线程执行完毕,就将计数器减 1 (countdownlatch.countDown()),当计数器的值变为 0 时,在 CountDownLatch 上 await() 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。 - 实现多个线程开始执行任务的最大并行性。
注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 CountDownLatch 对象,将其计数器初始化为 1 (new CountDownLatch(1)),多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。
4.2.CountDownLatch示例
/**
*
* @Description: CountDownLatch 使用方法示例
*/
public class CountDownLatchExample1 {
// 请求的数量
private static final int threadCount = 550;
public static void main(String[] args) throws InterruptedException {
// 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢)
ExecutorService threadPool = Executors.newFixedThreadPool(300);
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
final int threadnum = i;
threadPool.execute(() -> {// Lambda 表达式的运用
try {
test(threadnum);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
countDownLatch.countDown();// 表示一个请求已经被完成
}
});
}
countDownLatch.await();
threadPool.shutdown();
System.out.println("finish");
}
public static void test(int threadnum) throws InterruptedException {
Thread.sleep(1000);// 模拟请求的耗时操作
System.out.println("threadnum:" + threadnum);
Thread.sleep(1000);// 模拟请求的耗时操作
}
}
上面的代码中,我们定义了请求的数量为 550,当这 550 个请求被处理完成之后,才会执行System.out.println(“finish”);。
与 CountDownLatch 的第一次交互是主线程等待其他线程。主线程必须在启动其他线程后立即调用 CountDownLatch.await() 方法。这样主线程的操作就会在这个方法上阻塞,直到其他线程完成各自的任务。
其他 N 个线程必须引用闭锁对象,因为他们需要通知 CountDownLatch 对象,他们已经完成了各自的任务。这种通知机制是通过 CountDownLatch.countDown()方法来完成的;每调用一次这个方法,在构造函数中初始化的 count 值就减 1。所以当 N 个线程都调 用了这个方法,count 的值等于 0,然后主线程就能通过 await()方法,恢复执行自己的任务。
再插一嘴:CountDownLatch 的 await() 方法使用不当很容易产生死锁,比如我们上面代码中的 for 循环改为:
for (int i = 0; i < threadCount-1; i++) {
.......
}
这样就导致 count 的值没办法等于 0,然后就会导致一直等待。
4.3.CountDownLatch缺陷
CountDownLatch 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch 使用完毕后,它不能再次被使用。
4.4.CountDownLatch常见面试题?
- ountDownLatch 怎么用?应用场景是什么?
- CountDownLatch 和 CyclicBarrier 的不同之处?
- CountDownLatch 类中主要的方法?
解答:
- 在实际开发场景中,很多情况下需要我们初始化一系列的前置操作,比如数据库先建立连接,所有bean都加载完毕,在这些准备条件都完成之前,是不能进行下一步工作的,所以这就是利用 CountDownLatch 的一个很好场景,我们可以让应用程序的主线程在其他线程都准备完毕之后再继续执行。
- CountDownLatch 是计数器,只能使用一次,而 CyclicBarrier 的计数器提供 reset 功能,可以多次使用。CountDownLatch主要是一个或者多个线程等待,CyclicBarrier则是多个线程之间相互等待。
- countDown()、await()方法
5.CyclicBarrier(循环栅栏)
CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。
CountDownLatch 的实现是基于 AQS 的,而 CycliBarrier 是基于 ReentrantLock(ReentrantLock 也属于 AQS 同步器)和 Condition 的。
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用 await() 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
再来看一下它的构造函数:
public CyclicBarrier(int parties) {
this(parties, null);
}
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
其中,parties 就代表了有拦截的线程的数量,当拦截的线程数量达到这个值的时候就打开栅栏,让所有线程通过。
5.1.CyclicBarrier的应用场景
CyclicBarrier 可以用于多线程计算数据,最后合并计算结果的应用场景。比如我们用一个 Excel 保存了用户所有银行流水,每个 Sheet 保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个 sheet 里的银行流水,都执行完之后,得到每个 sheet 的日均银行流水,最后,再用 barrierAction 用这些线程的计算结果,计算出整个 Excel 的日均银行流水。
5.2.CyclicBarrier示例
/**
* @Description: 测试 CyclicBarrier 类中带参数的 await() 方法
*/
public class CyclicBarrierExample2 {
// 请求的数量
private static final int threadCount = 550;
// 需要同步的线程数量
private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
public static void main(String[] args) throws InterruptedException {
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
Thread.sleep(1000);
threadPool.execute(() -> {
try {
test(threadNum);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (BrokenBarrierException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
threadPool.shutdown();
}
public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
System.out.println("threadnum:" + threadnum + "is ready");
try {
/**等待60秒,保证子线程完全执行结束*/
cyclicBarrier.await(60, TimeUnit.SECONDS);
} catch (Exception e) {
System.out.println("-----CyclicBarrierException------");
}
System.out.println("threadnum:" + threadnum + "is finish");
}
}
threadnum:0is ready
threadnum:1is ready
threadnum:2is ready
threadnum:3is ready
threadnum:4is ready
threadnum:4is finish
threadnum:0is finish
threadnum:1is finish
threadnum:2is finish
threadnum:3is finish
threadnum:5is ready
threadnum:6is ready
threadnum:7is ready
threadnum:8is ready
threadnum:9is ready
threadnum:9is finish
threadnum:5is finish
threadnum:8is finish
threadnum:7is finish
threadnum:6is finish
......
可以看到当线程数量也就是请求数量达到我们定义的 5 个的时候, await() 方法之后的方法才被执行。
另外,CyclicBarrier 还提供一个更高级的构造函数 CyclicBarrier(int parties, Runnable barrierAction),用于在线程到达屏障时,优先执行 barrierAction,方便处理更复杂的业务场景。示例代码如下:
/**
* @Description: 新建 CyclicBarrier 的时候指定一个 Runnable
*/
public class CyclicBarrierExample3 {
// 请求的数量
private static final int threadCount = 550;
// 需要同步的线程数量
private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> {
System.out.println("------当线程数达到之后,优先执行------");
});
public static void main(String[] args) throws InterruptedException {
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
Thread.sleep(1000);
threadPool.execute(() -> {
try {
test(threadNum);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (BrokenBarrierException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
threadPool.shutdown();
}
public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
System.out.println("threadnum:" + threadnum + "is ready");
cyclicBarrier.await();
System.out.println("threadnum:" + threadnum + "is finish");
}
}
运行结果:
threadnum:0is ready
threadnum:1is ready
threadnum:2is ready
threadnum:3is ready
threadnum:4is ready
------当线程数达到之后,优先执行------
threadnum:4is finish
threadnum:0is finish
threadnum:2is finish
threadnum:1is finish
threadnum:3is finish
threadnum:5is ready
threadnum:6is ready
threadnum:7is ready
threadnum:8is ready
threadnum:9is ready
------当线程数达到之后,优先执行------
threadnum:9is finish
threadnum:5is finish
threadnum:6is finish
threadnum:8is finish
threadnum:7is finish
......
5.3.CyclicBarrier源码分析
当调用 CyclicBarrier 对象调用 await() 方法时,实际上调用的是 dowait(false, 0L)方法。 await() 方法就像树立起一个栅栏的行为一样,将线程挡住了,当拦住的线程数量达到 parties 的值时,栅栏才会打开,线程才得以通过执行。
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
// 当线程数量或者请求数量达到 count 时 await 之后的方法才会被执行。上面的示例中 count 的值就为 5。
private int count;
/**
* Main barrier code, covering the various policies.
*/
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();
}
// cout减1
int index = --count;
// 当 count 数量减为 0 之后说明最后一个线程已经到达栅栏了,也就是达到了可以执行await 方法之后的条件
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
// 将 count 重置为 parties 属性的初始化值
// 唤醒之前等待的线程
// 下一波执行开始
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
// loop until tripped, broken, interrupted, or timed out
for (;;) {
try {
if (!timed)
trip.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();
}
}
总结:CyclicBarrier 内部通过一个 count 变量作为计数器,count 的初始值为 parties 属性的初始化值,每当一个线程到了栅栏这里了,那么就将计数器减一。如果 count 值为 0 了,表示这是这一代最后一个线程到达栅栏,就尝试执行我们构造方法中输入的任务。
5.4.CyclicBarrier和CountDownLatch的区别?
CountDownLatch 是计数器,只能使用一次,而 CyclicBarrier 的计数器提供 reset 功能,可以多次使用。但是我不那么认为它们之间的区别仅仅就是这么简单的一点。我们来从 jdk 作者设计的目的来看,javadoc 是这么描述它们的:
CountDownLatch: A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.(CountDownLatch: 一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;) CyclicBarrier : A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point.(CyclicBarrier : 多个线程互相等待,直到到达同一个同步点,再继续一起执行。)
对于 CountDownLatch 来说,重点是“一个线程(多个线程)等待”,而其他的 N 个线程在完成“某件事情”之后,可以终止,也可以等待。而对于 CyclicBarrier,“多个线程相互等待”,重点是多个线程,在任意一个线程没有完6成,所有的线程都必全部完成。