6、AQS(AbstractQueuedSynchronizer)
1、简介
AQS ,AbstractQueuedSynchronizer ,即抽象队列同步器。它是构建锁或者其他同步组件的基础框架(如 ReentrantLock、ReentrantReadWriteLock、Semaphore 等),J.U.C 并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。
它是 J.U.C 并发包中的核心基础组件。
2、优势
AQS 解决了在实现同步器时涉及到的大量细节问题,例如获取同步状态、FIFO 同步队列。基于 AQS 来构建同步器可以带来很多好处。它不仅能够极大地减少实现工作,而且也不必处理在多个位置上发生的竞争问题。
在基于 AQS 构建的同步器中,只能在一个时刻发生阻塞,从而降低上下文切换的开销,提高了吞吐量。同时在设计 AQS 时充分考虑了可伸缩性,因此 J.U.C 中,所有基于 AQS 构建的同步器均可以获得这个优势。
3、同步状态
AQS 的主要使用方式是继承,子类通过继承同步器,并实现它的抽象方法来管理同步状态。
AQS 使用一个 int 类型的成员变量 state 来表示同步状态:
- 当 state > 0 时,表示已经获取了锁。
- 当 state = 0 时,表示释放了锁。
它提供了三个方法,来对同步状态 state 进行操作,并且 AQS 可以确保对 state 的操作是安全的:
- #getState()
- #setState(int newState)
- #compareAndSetState(int expect, int update)
AQS的设计模式采用的是模板方法模式。子类通过继承的方式,实现它的抽象方法来管理同步状态。对于子类而言,它并没有太多的活要做,AQS已经提供了大量的模板方法来实现同步,主要是分为三类:
1、独占式获取和释放同步状态
2、共享式获取和释放同步状态
3、查询同步队列中的等待线程情况。
独占式
独占式,同一时刻,仅有一个线程持有同步状态。
独占式同步状态获取
#acquire(int arg) 方法,为 AQS 提供的模板方法。该方法为独占式获取同步状态,但是该方法对中断不敏感。也就是说,由于线程获取同步状态失败而加入到 CLH 同步队列中,后续对该线程进行中断操作时,线程不会从 CLH 同步队列中移除。代码如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
调用 #tryAcquire(int arg) 方法,去尝试获取同步状态,获取成功则设置锁状态并返回 true ,否则获取失败,返回 false 。若获取成功,#acquire(int arg) 方法,直接返回,不用线程阻塞,自旋直到获得同步状态成功。
如果 #tryAcquire(int arg) 方法返回 false ,即获取同步状态失败,则调用 #addWaiter(Node mode) 方法,将当前线程加入到 CLH 同步队列尾部。并且, mode 方法参数为 Node.EXCLUSIVE ,表示独占模式。
调用 boolean #acquireQueued(Node node, int arg) 方法,自旋直到获得同步状态成功。
另外,该方法的返回值类型为 boolean ,当返回 true 时,表示在这个过程中,发生过线程中断。但是呢,这个方法又会清理线程中断的标识,所以在种情况下,需要调用 #selfInterrupt() 方法,恢复线程中断的标识,代码如下:
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
acquireQueued
boolean #acquireQueued(Node node, int arg) 方法,为一个自旋的过程,也就是说,当前线程(Node)进入同步队列后,就会进入一个自旋的过程,每个节点都会自省地观察,当条件满足,获取到同步状态后,就可以从这个自旋过程中退出,否则会一直执行下去。
1: final boolean acquireQueued(final Node node, int arg) {
2: // 记录是否获取同步状态成功
3: boolean failed = true;
4: try {
5: // 记录过程中,是否发生线程中断
6: boolean interrupted = false;
7: /*
8: * 自旋过程,其实就是一个死循环而已
9: */
10: for (;;) {
11: // 当前线程的前驱节点
12: final Node p = node.predecessor();
13: // 当前线程的前驱节点是头结点,且同步状态成功
14: if (p == head && tryAcquire(arg)) {
15: setHead(node);
16: p.next = null; // help GC
17: failed = false;
18: return interrupted;
19: }
20: // 获取失败,线程等待--具体后面介绍
21: if (shouldParkAfterFailedAcquire(p, node) &&
22: parkAndCheckInterrupt())
23: interrupted = true;
24: }
25: } finally {
26: // 获取同步状态发生异常,取消获取。
27: if (failed)
28: cancelAcquire(node);
29: }
30: }
独占式获取响应中断
AQS 提供了acquire(int arg) 方法,以供独占式获取同步状态,但是该方法对中断不响应,对线程进行中断操作后,该线程会依然位于CLH同步队列中,等待着获取同步状态。为了响应中断,AQS 提供了 #acquireInterruptibly(int arg) 方法。该方法在等待获取同步状态时,如果当前线程被中断了,会立刻响应中断,并抛出 InterruptedException 异常。
public final void acquireInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
它与 #acquire(int arg) 方法仅有两个差别:
1、方法声明抛出 InterruptedException 异常。
2、在中断方法处不再是使用 interrupted 标志,而是直接抛出 InterruptedException 异常。
独占式超时获取
AQS 除了提供上面两个方法外,还提供了一个增强版的方法 #tryAcquireNanos(int arg, long nanos) 。该方法为 #acquireInterruptibly(int arg) 方法的进一步增强,它除了响应中断外,还有超时控制。即如果当前线程没有在指定时间内获取同步状态,则会返回 false ,否则返回 true 。
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
独占式同步状态释放
当线程获取同步状态后,执行完相应逻辑后,就需要释放同步状态。AQS 提供了#release(int arg)方法,释放同步状态。代码如下:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
总结
在 AQS 中维护着一个 FIFO 的同步队列。
- 当线程获取同步状态失败后,则会加入到这个 CLH 同步队列的对尾,并一直保持着自旋。
- 在 CLH 同步队列中的线程在自旋时,会判断其前驱节点是否为首节点,如果为首节点则不断尝试获取同步状态,获取成功则退出CLH同步队列。
- 当线程执行完逻辑后,会释放同步状态,释放后会唤醒其后继节点。
共享式
共享式与独占式的最主要区别在于,同一时刻:
- 独占式只能有一个线程获取同步状态。
- 共享式可以有多个线程获取同步状态。
例如,读操作可以有多个线程同时进行,而写操作同一时刻只能有一个线程进行写操作,其他操作都会被阻塞。参见 ReentrantReadWriteLock 。
共享式同步状态获取
AQS 提供 #acquireShared(int arg) 方法,共享式获取同步状态。代码如下:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
1: private void doAcquireShared(int arg) {
2: // 共享式节点
3: final Node node = addWaiter(Node.SHARED);
4: boolean failed = true;
5: try {
6: boolean interrupted = false;
7: for (;;) {
8: // 前驱节点
9: final Node p = node.predecessor();
10: // 如果其前驱节点,获取同步状态
11: if (p == head) {
12: // 尝试获取同步
13: int r = tryAcquireShared(arg);
14: if (r >= 0) {
15: setHeadAndPropagate(node, r);
16: p.next = null; // help GC
17: if (interrupted)
18: selfInterrupt();
19: failed = false;
20: return;
21: }
22: }
23: if (shouldParkAfterFailedAcquire(p, node) &&
24: parkAndCheckInterrupt())
25: interrupted = true;
26: }
27: } finally {
28: if (failed)
29: cancelAcquire(node);
30: }
31: }
调用 #tryAcquireShared(int arg) 方法,尝试获取同步状态,获取成功则设置锁状态并返回大于等于 0 ,否则获取失败,返回小于 0 。若获取成功,直接返回,不用线程阻塞,自旋直到获得同步状态成功。
4、同步队列
AQS 通过内置的 FIFO 同步队列来完成资源获取线程的排队工作:
- 如果当前线程获取同步状态失败(锁)时,AQS 则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程。
- 当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。
节点:
在CLH同步队列中,一个节点(Node),代表一个线程,它保存着线程的引用(thread)、状态(waitstatus)、前驱节点(prev)、后继节点(next)。其定义如下:
Node 是 AbstractQueuedSynchronizer 的内部静态类。
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;
/**
* 节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()后,该节点将会从等待队列中转移到同步队列中,加入到同步状态的获取中
*/
static final int CONDITION = -2;
/**
* 表示下一次共享式同步状态获取,将会无条件地传播下去
*/
static final int PROPAGATE = -3;
/** 等待状态 */
volatile int waitStatus;
/** 前驱节点,当节点添加到同步队列时被设置(尾部添加) */
volatile Node prev;
/** 后继节点 */
volatile Node next;
/** 等待队列中的后续节点。如果当前节点是共享的,那么字段将是一个 SHARED 常量,也就是说节点类型(独占和共享)和等待队列中的后续节点共用同一个字段 */
Node nextWaiter;
/** 获取同步状态的线程 */
volatile Thread thread;
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;
}
}
waitstatus字段,等待字段,用来控制线程的阻塞和唤醒,并且可以避免不必要的调用LockSupport的park()方法和unpark()方法。目前有5种:INITAL(初始状态)、CANCELLED、SIGNAL、CONDITION、PROPAGATE。
CLH同步队列,结构图如下:
- head 和 tail 字段,是 AbstractQueuedSynchronizer 的字段,分别指向同步队列的头和尾。
- prev 和 next 字段,分别指向 Node 节点的前一个和后一个 Node 节点,从而实现链式双向队列。再配合上 head 和 tail 字段,快速定位到同步队列的头尾。
- thread 字段,Node 节点对应的线程 Thread 。
- nextWaiter 字段,Node 节点获取同步状态的模型( Mode )。#tryAcquire(int args) 和 #tryAcquireShared(int args) 方法,分别是独占式和共享式获取同步状态。在获取失败时,它们都会调用 #addWaiter(Node mode) 方法入队。nextWaiter 就是用来表示是哪种模式:(1)SHARED 静态 + 不可变字段,枚举共享模式。(2)EXCLUSIVE 静态 + 不可变字段,枚举独占模式。(3)#isShared() 方法,判断是否为共享式获取同步状态。
- #predecessor() 方法,获得 Node 节点的前一个 Node 节点。在方法的内部,Node p = prev 的本地拷贝,是为了避免并发情况下,prev 判断完 == null 时,恰好被修改,从而保证线程安全。
入列:
1、tail 指向新节点。
2、新节点的 prev 指向当前最后的节点。
3、当前最后一个节点的 next 指向当前节点。
但是,实际上,入队逻辑实现的 #addWaiter(Node) 方法,需要考虑并发的情况。它通过 CAS 的方式,来保证正确的添加 Node 。代码如下:
private Node addWaiter(Node mode) {
// 新建节点
Node node = new Node(Thread.currentThread(), mode);
// 记录原尾节点
Node pred = tail;
// 快速尝试,添加新节点为尾节点
if (pred != null) {
// 设置新 Node 节点的尾节点为原尾节点
node.prev = pred;
// CAS 设置新的尾节点
if (compareAndSetTail(pred, node)) {
// 成功,原尾节点的下一个节点为新节点
pred.next = node;
return node;
}
}
// 失败,多次尝试,直到成功
enq(node);
return node;
}
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long tailOffset = unsafe.objectFieldOffset (AbstractQueuedSynchronizer.class.getDeclaredField("tail")); // 这块代码,实际在 static 代码块,此处为了方便理解,做了简化。
private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
private Node enq(final Node node) {
// 多次尝试,直到成功为止
for (;;) {
// 记录原尾节点
Node t = tail;
// 原尾节点不存在,创建首尾节点都为 new Node()
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
// 原尾节点存在,添加新节点为尾节点
} else {
//设置为尾节点
node.prev = t;
// CAS 设置新的尾节点
if (compareAndSetTail(t, node)) {
// 成功,原尾节点的下一个节点为新节点
t.next = node;
return t;
}
}
}
}
出列:
CLH 同步队列遵循 FIFO,首节点的线程释放同步状态后,将会唤醒它的下一个节点(Node.next)。而后继节点将会在获取同步状态成功时,将自己设置为首节点( head )。
这个过程非常简单,head 执行该节点并断开原首节点的 next 和当前节点的 prev 即可。注意,在这个过程是不需要使用 CAS 来保证的,因为只有一个线程,能够成功获取到同步状态。
#setHead(Node node) 方法,实现上述的出列逻辑。代码如下:
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
5、主要的内置方法
AQS 主要提供了如下方法:
1、#getState():返回同步状态的当前值。
2、#setState(int newState):设置当前同步状态。
3、#compareAndSetState(int expect, int update):使用 CAS 设置当前状态,该方法能够保证状态设置的原子性。
4、【可重写】#tryAcquire(int arg):独占式获取同步状态,获取同步状态成功后,其他线程需要等待该线程释放同步状态才能获取同步状态。
5、【可重写】#tryRelease(int arg):独占式释放同步状态。
6、【可重写】#tryAcquireShared(int arg):共享式获取同步状态,返回值大于等于 0 ,则表示获取成功;否则,获取失败。
7、【可重写】#tryReleaseShared(int arg):共享式释放同步状态。
8、【可重写】#isHeldExclusively():当前同步器是否在独占式模式下被线程占用,一般该方法表示是否被当前线程所独占。
9、acquire(int arg):独占式获取同步状态。如果当前线程获取同步状态成功,则由该方法返回;否则,将会进入同步队列等待。该方法将会调用可重写的 #tryAcquire(int arg) 方法;
10、#acquireInterruptibly(int arg):与 #acquire(int arg) 相同,但是该方法响应中断。当前线程为获取到同步状态而进入到同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException 异常并返回。
11、#tryAcquireNanos(int arg, long nanos):超时获取同步状态。如果当前线程在 nanos 时间内没有获取到同步状态,那么将会返回 false ,已经获取则返回 true 。
12、#acquireShared(int arg):共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态;
13、#acquireSharedInterruptibly(int arg):共享式获取同步状态,响应中断。
14、#tryAcquireSharedNanos(int arg, long nanosTimeout):共享式获取同步状态,增加超时限制。
15、#release(int arg):独占式释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒。
16、#releaseShared(int arg):共享式释放同步状态。
从上面的方法看下来,基本上可以分成3类:
1、独占式获取与释放同步状态。
2、共享式释放与获取同步状态。
3、查询同步队列中等待线程情况。
7、J.U.C 之并发工具类:CyclicBarrier
简介
CyclicBarrier,它允许一组线程互相等待,直到达到某个公共屏障点(Common Barrier Point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时CyclicBarrier在释放等待线程后,可以重用,所以称它为循环(Cyclic)的屏障(Barrier)。
通俗点讲就是:让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
实现分析
java.util.concurrent.CyclicBarrier 的结构如下:
通过上图,我们可以看到 CyclicBarrier 的内部是使用重入锁 ReentrantLock 和 Condition 。
它有两个构造函数:
- CyclicBarrier(int parties):创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,但它不会在启动 barrier 时执行预定义的操作。
- CyclicBarrier(int parties, Runnable barrierAction) :创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动 barrier 时执行给定的屏障操作,该操作由最后一个进入 barrier 的线程执行。
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);
}
await方法
每个线程调用 #await() 方法,告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。当所有线程都到达了屏障,结束阻塞,所有线程可继续执行后续逻辑。
如果该线程不是到达的最后一个线程,则他会一直处于等待状态,除非发生以下情况:
1、最后一个线程到达,即 index == 0 。
2、超出了指定时间(超时等待)。
3、其他的某个线程中断当前线程。
4、其他的某个线程中断另一个等待的线程。
5、其他的某个线程在等待 barrier 超时。
6、其他的某个线程在此 barrier 调用 #reset() 方法。#reset() 方法,用于将屏障重置为初始状态。
应用场景
CyclicBarrier 适用于多线程结果合并的操作,用于多线程计算数据,最后合并计算结果的应用场景。比如,我们需要统计多个 Excel 中的数据,然后等到一个总结果。我们可以通过多线程处理每一个 Excel ,执行完成后得到相应的结果,最后通过 barrierAction 来计算这些线程的计算结果,得到所有Excel的总和。
应用示例
比如我们开会只有等所有的人到齐了才会开会,如下:
public class CyclicBarrierTest {
private static CyclicBarrier cyclicBarrier;
static class CyclicBarrierThread extends Thread{
public void run() {
System.out.println(Thread.currentThread().getName() + "到了");
//等待
try {
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args){
cyclicBarrier = new CyclicBarrier(5, new Runnable() {
@Override
public void run() {
System.out.println("人到齐了,开会吧....");
}
});
for(int i = 0 ; i < 5 ; i++){
new CyclicBarrierThread().start();
}
}
}
J.U.C 之并发工具类:CountDownLatch
简介
CyclicBarrier 所描述的是“允许一组线程互相等待,直到到达某个公共屏障点,才会进行后续任务,而 CountDownLatch 所描述的是“在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待”。
用给定的计数初始化 CountDownLatch。由于调用了 #countDown() 方法,所以在当前计数到达零之前,#await() 方法会一直受阻塞。之后,会释放所有等待的线程,#await() 的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier 。
CountDownLatch 是通过一个计数器来实现的,当我们在 new 一个 CountDownLatch 对象的时候,需要带入该计数器值,该值就表示了线程的数量。
- 每当一个线程完成自己的任务后,计数器的值就会减 1 。
- 当计数器的值变为0时,就表示所有的线程均已经完成了任务,然后就可以恢复等待的线程继续执行了。
虽然,CountDownLatch 与 CyclicBarrier 有那么点相似,但是他们还是存在一些区别的:
- CountDownLatch 的作用是允许 1 或 N 个线程等待其他线程完成执行;而 CyclicBarrier 则是允许 N 个线程相互等待。
- CountDownLatch 的计数器无法被重置;CyclicBarrier 的计数器可以被重置后使用,因此它被称为是循环的 barrier 。
实现分析
java.util.concurrent.CountDownLatch 结构如下图:
通过上面的结构图我们可以看到,CountDownLatch 内部依赖 Sync 实现,而 Sync 继承 AQS 。
CountDownLatch 仅提供了一个构造方法,代码如下:
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
Sync
sync 变量,为 CountDownLatch 的一个内部类 Sync :
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
// 获取同步状态
int getCount() {
return getState();
}
// 获取同步状态
@Override
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
// 释放同步状态
@Override
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
}
await
CountDownLatch 提供 #await() 方法,来使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。
countDown
CountDownLatch 提供 #countDown() 方法,递减锁存器的计数。如果计数到达零,则唤醒所有等待的线程。
public void countDown() {
sync.releaseShared(1);
}
// AQS.java
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
// Sync.java
@Overrride
protected boolean tryReleaseShared(int releases) {
for (;;) {
//获取锁状态
int c = getState();
//c == 0 直接返回,释放锁成功
if (c == 0)
return false;
//计算新“锁计数器”
int nextc = c-1;
//更新锁状态(计数器)
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
总结
CountDownLatch 内部通过共享锁实现。
- 在创建 CountDownLatch 实例时,需要传递一个int型的参数:count,该参数为计数器的初始值,也可以理解为该共享锁可以获取的总次数。
- 当某个线程调用 #await() 方法,程序首先判断 count 的值是否为 0 ,如果不为 0 的话,则会一直等待直到为 0 为止。
- 当其他线程调用 #countDown() 方法时,则执行释放共享锁状态,使 count 值 - 1。
- 当在创建 CountDownLatch 时初始化的 count 参数,必须要有 count 线程调用#countDown() 方法,才会使计数器 count 等于 0 ,锁才会释放,前面等待的线程才会继续运行。
注意 CountDownLatch 不能回滚重置。
应用示例
示例仍然使用开会案例。老板进入会议室等待 5 个人全部到达会议室才会开会。所以这里有两种线程:老板等待开会线程、员工到达会议室线程:
public class CountDownLatchTest {
private static CountDownLatch countDownLatch = new CountDownLatch(5);
/**
* Boss线程,等待员工到达开会
*/
static class BossThread extends Thread{
@Override
public void run() {
System.out.println("Boss在会议室等待,总共有" + countDownLatch.getCount() + "个人开会...");
try {
//Boss等待
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("所有人都已经到齐了,开会吧...");
}
}
// 员工到达会议室线程
static class EmpleoyeeThread extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ",到达会议室....");
//员工到达会议室 count - 1
countDownLatch.countDown();
}
}
public static void main(String[] args){
//Boss线程启动
new BossThread().start();
for(int i = 0 ; i < countDownLatch.getCount() ; i++){
new EmpleoyeeThread().start();
}
}
}
8、J.U.C 之并发工具类:Semaphore
简介
信号量 Semaphore 是一个控制访问多个共享资源的计数器,和 CountDownLatch 一样,其本质上是一个“共享锁”。
Semaphore 通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。
下面我们就一个停车场的简单例子来阐述 Semaphore :
1、为了简单起见我们假设停车场仅有 5 个停车位。一开始停车场没有车辆所有车位全部空着,然后先后到来三辆车,停车场车位够,安排进去停车,然后又来三辆,这个时候由于只有两个停车位,所有只能停两辆,其余一辆必须在外面候着,直到停车场有空车位。当然,以后每来一辆都需要在外面候着。当停车场有车开出去,里面有空位了,则安排一辆车进去(至于是哪辆,要看选择的机制是公平还是非公平)。
2、从程序角度看,停车场就相当于信号量 Semaphore ,其中许可数为 5 ,车辆就相对线程。当来一辆车时,许可数就会减 1 。当停车场没有车位了(许可数 == 0 ),其他来的车辆需要在外面等候着。如果有一辆车开出停车场,许可数 + 1,然后放进来一辆车。
3、信号量 Semaphore 是一个非负整数( >=1 )。当一个线程想要访问某个共享资源时,它必须要先获取 Semaphore。当 Semaphore > 0 时,获取该资源并使 Semaphore – 1 。如果S emaphore 值 = 0,则表示全部的共享资源已经被其他线程全部占用,线程必须要等待其他线程释放资源。当线程释放资源时,Semaphore 则 +1 。
实现分析
java.util.concurrent.Semaphore 结构如下图:
从上图可以看出,Semaphore 内部包含公平锁(FairSync)和非公平锁(NonfairSync,默认),继承内部类 Sync ,其中 Sync 继承 AQS(再一次阐述 AQS 的重要性)。
Semaphore 提供了两个构造函数:
Semaphore(int permits) :创建具有给定的许可数和非公平的公平设置的 Semaphore 。
Semaphore(int permits, boolean fair) :创建具有给定的许可数和给定的公平设置的 Semaphore 。
- Semaphore 默认选择非公平锁。
- 当信号量 Semaphore = 1 时,它可以当作互斥锁使用。其中 0、1 就相当于它的状态:1)当 =1 时表示,其他线程可以获取;2)当 =0 时,排他,即其他线程必须要等待。
- Semaphore 的代码实现结构,和 ReentrantLock 类似。
信号量获取
Semaphore 提供了 #acquire() 方法,来获取一个许可。
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
公平情况的 FairSync 的方法实现,代码如下:
// FairSync.java
@Override
protected int tryAcquireShared(int acquires) {
for (;;) {
//判断该线程是否位于CLH队列的列头,从而实现公平锁
if (hasQueuedPredecessors())
return -1;
//获取当前的信号量许可
int available = getState();
//设置“获得acquires个信号量许可之后,剩余的信号量许可数”
int remaining = available - acquires;
//CAS设置信号量
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
通过 #hasQueuedPredecessors() 方法,判断该线程是否位于 CLH 队列的列头,从而实现公平锁。
非公平情况的 NonfairSync 的方法实现,代码如下:
// NonfairSync.java
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
// Sync.java
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
对于非公平而言,因为它不需要判断当前线程是否位于 CLH 同步队列列头,所以相对而言会简单些。
信号量释放
获取了许可,当用完之后就需要释放,Semaphore 提供 #release() 方法,来释放许可。代码如下:
public void release() {
sync.releaseShared(1);
}
内部调用 AQS 的 #releaseShared(int arg) 方法,释放同步状态。
// AQS.java
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
releaseShared(int arg) 方法,会调用 Semaphore 内部类 Sync 的 #tryReleaseShared(int arg) 方法,释放同步状态。
// Sync.java
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
//信号量的许可数 = 当前信号许可数 + 待释放的信号许可数
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
//设置可获取的信号许可数为next
if (compareAndSetState(current, next))
return true;
}
}
如该方法返回 true 时,代表释放同步状态成功,从而在 #releaseShared(int args) 方法中,调用 #doReleaseShared() 方法,可唤醒阻塞等待 Semaphore 的许可的线程。
应用示例
public class SemaphoreTest {
static class Parking {
//信号量
private Semaphore semaphore;
Parking(int count) {
semaphore = new Semaphore(count);
}
public void park() {
try {
//获取信号量
semaphore.acquire();
long time = (long) (Math.random() * 10);
System.out.println(Thread.currentThread().getName() + "进入停车场,停车" + time + "秒..." );
Thread.sleep(time);
System.out.println(Thread.currentThread().getName() + "开出停车场...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}
}
static class Car extends Thread {
Parking parking ;
Car(Parking parking){
this.parking = parking;
}
@Override
public void run() {
parking.park(); //进入停车场
}
}
public static void main(String[] args){
Parking parking = new Parking(3);
for(int i = 0 ; i < 5 ; i++){
new Car(parking).start();
}
}
}