文章目录
- 一、CountDownLatch
- 1、使用
- 2、类图
- 3、实现原理探究
- (1) void await() 方法
- (2) await(long timeout, TimeUnit unit) 方法
- (3) void countDown() 方法
- (4) getCount() 方法
- 二、回环屏障 CyclicBarrier
- 1、使用
- 2、类图
- 3、实现原理探究
- (1) int await() 方法
- (2) await(long timeout, TimeUnit unit) 方法
- (3) int dowait(boolean timed, long nanos) 方法
- 三、 信号量 Semaphore
- 1、使用
- 2、类图
- 3、实现原理
- (1) void acquire() 方法
- (2) void acquire(int permits) 方法
- (3)acquireUninterruptibly() 方法
- (4) acquireUninterruptibly(int permits) 方法
- (5) void release() 方法
- (6)release(int permits)
- 四、总结
一、CountDownLatch
在日常开发中,可能会遇到 需要在主线程中 开启多个线程 去执行任务,并且 主线程需要等待 所有子线程执行完毕后 再进行汇总 的情景。在 CountDownLatch 出现之前,一般都使用线程的 join() 方法来实现这一点,但是 join 不够灵活,不能够满足 不同场景的要求。
1、使用
例 👀 : CountDownLatch 的使用
package SYNC.CountDownLatchPack;
import java.util.concurrent.CountDownLatch;
public class JoinCountDownLatch {
// 创建一个 CountDownLatch 实例
private static volatile CountDownLatch countDownLatch = new CountDownLatch(2);
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
try{
Thread.sleep(1000);
System.out.println("child threadOne over!");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
countDownLatch.countDown();
}
}
});
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
try{
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
countDownLatch.countDown();
}
}
});
// 启动子线程
threadOne.start();
threadTwo.start();
System.out.println("wait all child thread over!");
// 等待子线程完毕后返回
countDownLatch.await();
System.out.println("all child thread over!");
}
}
运行结果:
例子中创建了一个 CountDownLatch 的实例,因为有两个子线程,所以 构造方法里 传入的参数 是 2。主方法里调用 countDownLatch.await();后会被阻塞。子线程调用 countDownLatch.countDown(); 方法,会使 CountDownLatch 内部的计数器减 1,两个子线程执行完毕后,计数器值会变为 0,这时 主线程的 await() 方法 才会返回。
其实以上代码不够优雅,在项目实践中 一般都避免直接操作线程,而是使用 ExecutorService 线程池来管理。 使用 ExecutorService 时传递的参数是 Runable 或者 Callable 对象,这时没办法直接调用线程的 join 方法,就需要选择使用 CountDownLatch 了。修改代码为:
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class JoinCountDownLatch {
/*
// 创建一个 CountDownLatch 实例
private static volatile CountDownLatch countDownLatch = new CountDownLatch(2);
public static void main(String[] args) throws InterruptedException {
Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
try{
Thread.sleep(1000);
System.out.println("child threadOne over!");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
countDownLatch.countDown();
}
}
});
Thread threadTwo = new Thread(new Runnable() {
@Override
public void run() {
try{
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
countDownLatch.countDown();
}
}
});
// 启动子线程
threadOne.start();
threadTwo.start();
System.out.println("wait all child thread over!");
// 等待子线程完毕后返回
countDownLatch.await();
System.out.println("all child thread over!");
}
*/
// 创建一个 CountDownLatch 实例
private static CountDownLatch countDownLatch = new CountDownLatch(2);
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
// 将线程 A 添加到线程池
executorService.submit(new Runnable() {
@Override
public void run() {
try{
Thread.sleep(1000);
System.out.println("child threadOne over!");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
countDownLatch.countDown();
}
}
});
// 将线程 B 添加到线程池
executorService.submit(new Runnable() {
@Override
public void run() {
try{
Thread.sleep(1000);
System.out.println("child threadTwo over!");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
countDownLatch.countDown();
}
}
});
System.out.println("wait all child thread over!");
// 等待子线程执行完毕,返回
countDownLatch.await();
System.out.println("all child thread over!");
executorService.shutdown();
}
}
🙌join 方法 与 CountDownLatch 方法的区别
调用一个子线程的 join() 方法后,该线程会一直阻塞 直到子线程运行完毕,而 CountDownLatch 则使用计数器来允许子线程运行完毕 或者 在运行中递减计数,也就是说,CountDownLatch 可以在子线程运行的任何时候让 await 方法返回,而不一定必须等到线程结束。
另外,使用线程池来管理线程时,一般都是直接添加 Runable 到线程池,这时候就没办法再调用线程的 join 方法啦, CountDownLatch 比 join 发 让我们对线程同步有更灵活的控制。
2、类图
构造方法:
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
// (一)
this.sync = new Sync(count);
}
(一):
Sync(int count) {
setState(count);
}
可以看到, CountDownLatch 的内部有个计数器,并且这个计数器是递减的。 CountDownLatch 是使用 AQS 实现的,构造方法中传入 count 值,实际上是把 计数器的值 count 赋给了 AQS 的状态变量 state,也就是说, 使用 AQS 的 状态值来表示计数器值。
3、实现原理探究
(1) void await() 方法
当线程调用 CountDownLatch 的 await 方法后,当前线程会被阻塞,直到以下情况才会返回:
- 当所有线程都调用了 CountDownLatch 对象的 countDown 方法后,也就是 计数器的值为 0 时。
- 其他线程调用了当前线程的 interrupt() 方法 中断了当前线程,当前线程会抛出 InterruptedException 异常,然后返回。
源码:
public void await() throws InterruptedException {
// (一)
sync.acquireSharedInterruptibly(1);
}
(一)acquireSharedInterruptibly:
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
// 如果线程被中断,则 抛出异常
if (Thread.interrupted())
throw new InterruptedException();
// (二) 如果当前计数值不为 0,则 进入 AQS 的队列等待
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
(二):
protected int tryAcquireShared(int acquires) {
// 查看当前状态值 即 计数器值
return (getState() == 0) ? 1 : -1;
}
可以看到,void await() 方法 的特点是 线程获取资源时 可以被中断加粗样式,并且 获取的资源是 共享资源。而且,tryAcquireShared 方法中传入的 arg 参数并没有被用到,调用该方法仅仅是检查当前状态值是不是 0,并没有调用 CAS 让当前状态值 -1。
(2) await(long timeout, TimeUnit unit) 方法
当线程调用 CountDownLatch 的 await 方法后,当前线程会被阻塞,直到以下情况才会返回:
- 当所有线程都调用了 CountDownLatch 对象的 countDown 方法后,也就是 计数器的值为 0 时。
- 其他线程调用了当前线程的 interrupt() 方法 中断了当前线程,当前线程会抛出 InterruptedException 异常,然后返回。
- 设置的 timeout 时间到了,因为超时而返回 false。
源码:
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
(3) void countDown() 方法
线程调用该方法后,计数器的值 递减,递减后,如果计数器的值为 0 则唤醒所有 因 调用 await 方法 而被阻塞的线程,否则 什么也不做。
源码:
public void countDown() {
// (一)
sync.releaseShared(1);
}
(一):
public final boolean releaseShared(int arg) {
// (二)
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
(二):
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
// 获取当前状态值 即 计数器值
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
可以看到,在循环中,首先获取当前状态值,即 计数器的值,如果状态值为 0 ,则返回 false,那么 countDown() 方法也返回。否则 是固态 CAS 将 计数器值 减1,CAS 失败则 循环重试,如果 CAS 成功了,再看 当前计数器值是否为 0,为 0 则返回 true,说明最后一个线程调用了 countDown 方法,那么,该线程除了让计数器值 减1 以外,还需要唤醒因为调用了 CountDownLatch 的 await 方法而被阻塞的线程。doReleaseShared();
方法会去激活阻塞的线程。
(4) getCount() 方法
获取当前计数器的值,也就是 AQS 的 state 的值。
源码:
public long getCount() {
return sync.getCount();
}
🎭 总结:
相比于 join,CountDownLatch 方法对线程同步有更灵活的控制,它是使用 AQS 实现的,使用 AQS 的状态值来存放计数器的值。首先在初始化 CountDownLatch 时 设置状态值(计数器值),当多个线程调用 countDown() 方法时 实际是原子性递减 AQS 的状态值。当线程调用 await() 方法后 当前线程会被放入 AQS 的阻塞队列,等待计数器为 0 再返回。其他线程调用 countdown 方法让计数器减 1 ,当计数器变为 0 ,当前线程会调用 AQS 的 doReleaseShared 方法 来激活 由于调用 await() 方法而被阻塞的线程。
二、回环屏障 CyclicBarrier
上述 CountDownLatch 的计数器是一次性的,也就是说,等到计数器值变为 0 后,再调用 CountDownLatch 的 await 和 countdown 方法都会立刻返回,这时就起不到线程同步的作用了。为了满足 计数器可以重置 的需要,提供了 CyclicBarrier。 它可以让一组线程全部达到一个状态后 再全部同时执行 。之所以叫 “回环”,是因为 当所有等待线程执行完毕,并重置 CyclicBarrier 的状态后,它可以被重用。之所以叫 “屏障”,是因为 线程调用 await 方法后就会被阻塞,这个阻塞点就叫 屏障点 ,等所有线程都调用了 await 方法后,线程们就会冲破屏障,继续向下执行。
1、使用
例 1👀:使用两个线程去执行一个被分解的任务 A,当两个线程把自己的任务都执行完毕后,再对它们的结果汇总处理。
import java.util.concurrent.*;
public class CyclicBarrierTest1 {
// 创建一个 CyclicBarrier 实例,添加一个 所有子线程 全部达到屏障后 执行的任务
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2, new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread() + "task1 merge result");
}
});
public static void main(String[] args) {
// 创建一个线程个数固定为 2 的线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);
// 将线程 A 添加到线程池
executorService.submit(new Runnable() {
@Override
public void run() {
try{
System.out.println(Thread.currentThread()+ "task1-1");
System.out.println(Thread.currentThread()+"enter in barrier");
cyclicBarrier.await();
System.out.println("enter to barrier");
} catch (Exception e) {
e.printStackTrace();
}
}
});
// 将线程 B 添加到线程池
executorService.submit(new Runnable() {
@Override
public void run() {
try{
System.out.println(Thread.currentThread()+ "task1-2");
System.out.println(Thread.currentThread()+"enter in barrier");
cyclicBarrier.await();
System.out.println("enter to barrier");
} catch (Exception e) {
e.printStackTrace();
}
}
});
// 关闭线程池
executorService.shutdown();
}
}
运行结果:
可以看到,创建了一个 CyclicBarrier 对象,第一个参数是 计数器初始值,第二个参数是 当计数器值为 0 时 需要执行的任务。在 main 方法中,首先创建了一个大小为 2 的线程池,然后添加两个子任务到线程池,每个任务在执行完自己的逻辑后,会调用 cyclicBarrier.await(); 。一开始,计数器值为 2,当 第一个线程调用 await 方法后,计数器值会递减为 1,这时由于计数器不为 0 ,所以当前线程 就到了屏障点, 而被阻塞。然后,第二个线程 调用 await 时,会进入屏障,计数器值也会递减,计数器变为 0 ,这时就会去执行 CyclicBarrier 构造方法里的任务,执行完毕后退出屏障点,并且唤醒被阻塞的第二个线程,第一个线程也会退出屏障点,继续向下运行。
这个例子说明,多个线程之间是互相等待的,假如 计数器值为 N,那么 随后调用 await 方法的 N-1 个线程 都会因为到达屏障点而被阻塞,当 第 N 个线程调用 await 方法后,计数器值变为 0 了,这时,第 N 个线程 才会发出通知,唤醒前面的 N-1 个线程。也就是 当全部线程到达屏障点时,才能一起继续向下执行。
例 2👀:CyclicBarrier 的可重用性
假设,一个任务由 阶段 1、阶段 2 和 阶段 3 组成,每个线程要串行地执行阶段 1、阶段 2 和 阶段 3,当多个线程执行该任务时,必须要保证所有线程的阶段 1 全部完成后 才能进入阶段 2,当所有线程的阶段 2 全部完成后,才能进入阶段 3 执行。
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CyclicBarrierTest2 {
// 创建一个 CyclicBarrier 实例
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
// 将线程 A 添加到线程池
executorService.submit(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread() + "step1");
cyclicBarrier.await();
System.out.println(Thread.currentThread() + "step2");
cyclicBarrier.await();
System.out.println(Thread.currentThread() + "step3");
}catch (Exception e){
e.printStackTrace();
}
}
});
// 将线程 B 添加到线程池
executorService.submit(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread() + "step1");
cyclicBarrier.await();
System.out.println(Thread.currentThread() + "step2");
cyclicBarrier.await();
System.out.println(Thread.currentThread() + "step3");
}catch (Exception e){
e.printStackTrace();
}
}
});
// 关闭线程池
executorService.shutdown();
}
}
以上代码中,每个子线程在执行完 step1 后都调用了 await 方法,等到所有线程都到达屏障点后,才会一起执行,这就保证了 所有线程都完成了阶段 1 后 才会开始执行阶段2 … … 以此类推。
运行结果:
2、类图
可以看到,CyclicBarrier 基于独占锁实现,本质底层还是基于 AQS 的。使用 lock 保证更新 计数器 count 的原子性 (另外使用 lock 的条件变量 trip 之处线程间使用 awiat 和 signal 操作进行同步)。parties 用于记录线程个数,表示多少线程调用 await 方法之后,所有线程才会冲破屏障,继续往下运行。而 count 一开始等于 parties,每当有线程调用 await 方法,count 就递减 1 ,当 count 为 0 时,就表示所有线程都到了屏障点。❓为s什么要维护 parties 和 count 两个变量呢 ❓ 因为 CycleBarrier 是可以被复用的,parties 始终记录总的线程个数,当 count 计数器值 变为 0 后,会将 parties 的值 赋给 count 从而进行复用。这两个变量在构造 CyclicBarrier 对象时传递的:
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
barrierCommand 是任务,当所有线程都到达屏障点后,就会执行这个任务。
在 变量 generation 内部有一个变量 broken,用来记录当前屏障 是否被打破,它没有被声明成 volatile ,因为是在锁内使用的,可以保证内存可见性:
private static class Generation {
boolean broken = false;
}
3、实现原理探究
(1) int await() 方法
当前线程调用该方法时会被阻塞,直到满足以下条件之一 才会返回:
- parties 个线程都调用了 await() 方法,也就是说 所有的线程都到了屏障点。
- 其他线程调用了该线程的 interrupt() 方法 中断了当前线程,则当前线程会抛出 InterruptedException 异常而返回。
- 与 当前屏障点 关联的 Generation 对象 的 broken 标志被设置为 true,会抛出 BrokenBarrierException 异常,然后返回。
源码:
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
可以看到,内部调用了 dowait() 方法,第一个参数是 false,表示 不设置超时时间,第二个参数没有意义。
(2) await(long timeout, TimeUnit unit) 方法
当前线程调用该方法时会被阻塞,直到满足以下条件之一 才会返回:
- parties 个线程都调用了 await() 方法,也就是说 所有的线程都到了屏障点。
- 其他线程调用了该线程的 interrupt() 方法 中断了当前线程,则当前线程会抛出 InterruptedException 异常而返回。
- 与 当前屏障点 关联的 Generation 对象 的 broken 标志被设置为 true,会抛出 BrokenBarrierException 异常,然后返回。
- 设置的超时时间到了后 返回 false。
源码:
public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
可以看到,内部调用了 dowait() 方法,第一个参数是 true,表示 设置了超时时间,第二个参数是超时时间。
(3) int dowait(boolean timed, long nanos) 方法
该方法是 CyclicBarrier 的核心。
源码:
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;
// 如果 index == 0,说明所有线程都到了屏障点
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
// 执行任务
command.run();
ranAction = true;
// (一)激活其他因为调用 await 方法而被阻塞的线程,并重置 CyclicBarrier
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();
}
}
(一)nextGeneration():
private void nextGeneration() {
// signal completion of last generation
trip.signalAll();
// set up next generation
count = parties;
generation = new Generation();
}
当一个线程调用了 dowait 方法后,首先会获取独占锁 lock,比如,创建 CyclicBarrier 时传递的参数是 10,那么后面 9 个调用线程会被阻塞。然后 当前获取到锁的线程 会对 计数器 count 进行递减操作,递减后 count = index = 9,因为 index != 0,所以 当前线程会走到 for 循环。如果当前线程调用的是无参数的 await 方法,则 timed = false,所以 当前线程会被放入 条件变量的 trip 的条件阻塞队列,当前线程会被挂起并释放获取的 lock 锁。如果调用的是 有参数的 await 方法 则 timed = true,然后 当前线程也会被放入条件变量的条件队列中 并释放锁,不同的是 当前线程会在指定时间超时后 自动被激活。 当第一个获取锁的线程由于被阻塞 释放锁后,被阻塞的 9 个线程中 有一个会释放 lock 锁,然后执行与第一个线程同样的操作,直到最后一个线程获取到 lock 锁,此时 已经有 9 个线程被放入了 条件变量 trip 的条件队列中。最后 count = index = 0,如果创建 CyclicBarrir 时传递了任务,则 在其他线程被唤醒前 先执行任务,任务执行完毕后,再激活其他因为调用 await 方法而被阻塞的 9 个线程,并 重置 CyclicBarrier ,然后 这 10 个线程就可以继续向下进行啦。
🎭 总结:
CyclicBarrier 与 CountDownLatch 的不同之处在于,前者是可以复用的,而且 特别适合 分段任务有序执行的场景 ,它是通过独占锁 ReentrantLock 实现计数器原子性更新,并使用条件变量队列 来实现线程同步。
三、 信号量 Semaphore
与上述两个同步器不同,✨ Semaphore 内部的计数器是递增的,并且在一开始初始化时 可以指定一个初始值,但是并不需要知道 需要同步的线程个数,而是在需要同步的地方调用 acquire 方法时 指定需要同步的线程个数。✨
1、使用
例 1 👀: 在主线程中开启两个子线程让他们执行,等所有子线程执行完毕后,主线程再继续向下执行:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
public class SemaphoreTest1 {
// 创建一个 Semaphore 实例
private static Semaphore semaphore= new Semaphore(0);
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
// 将线程 A 添加到线程池
executorService.submit(new Runnable() {
@Override
public void run() {
try{
System.out.println(Thread.currentThread() + " over");
semaphore.release();
}catch (Exception e){
e.printStackTrace();
}
}
});
// 将线程 B 添加到线程池
executorService.submit(new Runnable() {
@Override
public void run() {
try{
System.out.println(Thread.currentThread() + " over");
semaphore.release();
}catch (Exception e){
e.printStackTrace();
}
}
});
// 等待子线程执行完毕,返回
semaphore.acquire(2);
System.out.println("all child thread over!");
// 关闭线程池
executorService.shutdown();
}
}
首先创建了一个 信号量实例,构造方法的入参是 0,说明信号量计数器的值是 0。然后 main 方法向线程池添加两个线程任务,在每个线程内部调用信号量的 release 方法,相当于让计数器值递增 1,最后在 main 线程例调用信号量的 acquire 方法,传参为 2 表示 调用 acquire 方法的线程会一直阻塞,直到信号量的计数 变为 2 才会返回。所以,如果构造 Semaphore 时 传递的参数是 N,并在 M 个线程中 调用了该信号量的 release 方法,那么 在 调用 acquire 使 M 个线程同步时 传递的参数应该是 M+N 。
运行结果:
例 2 👀:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
public class SemaphoreTest2 {
// 创建一个 Semapore 实例
private static volatile Semaphore semaphore = new Semaphore(0);
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
// 将线程 A 添加到线程池
executorService.submit(new Runnable() {
@Override
public void run() {
try{
System.out.println(Thread.currentThread() + "A task over");
semaphore.release();
}catch (Exception e){
e.printStackTrace();
}
}
});
// 将线程 B 添加到线程池
executorService.submit(new Runnable() {
@Override
public void run() {
try{
System.out.println(Thread.currentThread() + "A task over");
semaphore.release();
}catch (Exception e){
e.printStackTrace();
}
}
});
// 等待子线程执行任务 A 完毕
semaphore.acquire(2);
// 将线程 C 添加到线程池
executorService.submit(new Runnable() {
@Override
public void run() {
try{
System.out.println(Thread.currentThread() + "B task over");
semaphore.release();
}catch (Exception e){
e.printStackTrace();
}
}
});
// 将线程 D 添加到线程池
executorService.submit(new Runnable() {
@Override
public void run() {
try{
System.out.println(Thread.currentThread() + "B task over");
semaphore.release();
}catch (Exception e){
e.printStackTrace();
}
}
});
// 等待子线程执行 B 完毕,返回
semaphore.acquire(2);
System.out.println("task is over");
// 关闭线程池
executorService.shutdown();
}
}
以上代码首先将线程 A 和 线程 B 加入线程池。主线程执行 semaphore.acquire(2); 后被阻塞,线程 A 和 线程 B 调用 release 方法后 信号量变成了 2 ,这时 主线程的acquire 方法会返回,返回后 信号量值为 0。然后主线程添加线程 C 和 线程 D到线程池,之后执行 semaphore.acquire(2); 被阻塞,当线程 C 和 D 执行完 release 方法后,主线程才返回。从一定程度上可以看出,Semaphore 在某种程度上 实现了 CyclicBarrier 的复用功能。
2、类图
可以看到,✨ Semaphore 也是使用 AQS 实现的,Sync 有两个实现类,用来指定获取信号量时 是否采用公平策略。AQS 的 state 值表示 当前持有的信号量个数。✨
构造方法源码,默认采用非公平策略:
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
3、实现原理
(1) void acquire() 方法
当前线程调用该方法的目的是 希望获取一个信号量资源。如果当前信号量个数大于 0 ,则 当前信号量的计数会 减 1,然后该方法直接返回。否则 如果当前信号量个数等于 0,则 当前线程会被放入 AQS 阻塞队列。当其他线程调用了当前线程的 interrupt() 方法中断了当前线程时,当前线程会抛出 InterruptedException 异常。
public void acquire() throws InterruptedException {
// (一)传入的参数 为 1 ,说明要获取一个信号量资源
sync.acquireSharedInterruptibly(1);
}
(一)acquireSharedInterruptibly:
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 调用 Sync 子类方法尝试获取,
if (tryAcquireShared(arg) < 0)
// 获取失败则放入阻塞队列,然后再次尝试,如果失败 则调用 park 方法挂起当前线程
doAcquireSharedInterruptibly(arg);
}
acquireSharedInterruptibly 里 调用了 Sync 子类的 tryAcquireShared 方法,这里的子类分为 非公平策略 NonfairSync 和 公平策略 FairSync。
- NonfairSync 的 tryAcquireShared 方法:
protected int tryAcquireShared(int acquires) {
// (二)
return nonfairTryAcquireShared(acquires);
}
(二)nonfairTryAcquireShared:
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
// 获取当前信号量值
int available = getState();
// 计算当前剩余值
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
可以看到,nonfairTryAcquireShared 方法 先获取当前信号量值,然后减去需要获取的值,得到 剩余的信号量个数。如果剩余值小于 0 说明 当前信号量个数满足不了需求,那么直接返回负数; 这时当前线程会被放入 AQS 阻塞队列。 如果剩余值大于 0 ,则使用 CAS 操作设置当前信号量为剩余值,然后返回剩余值。
由于 NonfairSync 是非公平获取的,也就是说 先调用 aquire 方法获取信号量的线程 不一定比 后来者 先获取到信号量。比如:线程 A 先调用了 acquire 方法获取信号量,但是 当前信号量个数为 0,那么 线程 A 会被放入 AQS 的阻塞队列。过一段时间后 线程 C 调用了 release 方法释放了一个信号量,如果当前没有其他线程获取信号量,那么 线程A 就会被激活,然后获取该信号量,但是,如果线程 C 释放信号量后,线程 C 调用了 acquire 方法,那么线程 C 和 线程 A 都会去竞争能够信号量资源。如果采用非公平策略,由 nonfairTryAcquireShared 可知,线程 C 完全可以在线程 A 被激活前 或者 激活后 先于线程 A 获取到该信号量,也就是说,在这种模式下,阻塞线程 和 当前请求的线程是竞争关系,而不遵循 先来先得的策略。
- FairSync 的 tryAcquireShared 方法:
protected int tryAcquireShared(int acquires) {
for (;;) {
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
可以看到,公平性还是靠 hasQueuedPredecessors() 方法来保证。公平策略是看 当前线程节点的前驱节点是否也在等待获取该资源,如果是 则自己放弃获取的权限,然后 当前线程会被放入 AQS 阻塞队列,否则 就去获取。
(2) void acquire(int permits) 方法
acquire() 方法只需要获取一个信号量值,而 acquire(int permits) 方法需要获取 permits 个。
源码:
public void acquire(int permits) throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireSharedInterruptibly(permits);
}
(3)acquireUninterruptibly() 方法
该方法与 acquire() 方法类似,不同在于 它对中断不响应。
public void acquireUninterruptibly() {
sync.acquireShared(1);
}
(4) acquireUninterruptibly(int permits) 方法
该方法与 acquire(int permits) 方法类似,不同在于 它对中断不响应。
public void acquireUninterruptibly(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireShared(permits);
}
(5) void release() 方法
该方法的作用是 把当前 Semaphore 对象的 信号量值 增加 1 ,如果当前有线程因为调用 acquire 方法被阻塞而被放入了 AQS 的阻塞队列,则 会根据公平策略 选择一个信号量个数能被满足的线程进行激活,激活的线程会尝试获取刚增加的信号量。
源码;
public void release() {
// (一)
sync.releaseShared(1);
}
(一)releaseShared :
public final boolean releaseShared(int arg) {
// (二)
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
可以看到,该方法先尝试释放资源,如果释放成功,则 调用 doReleaseShared(); ,会使用 unpark 方法 唤醒 AQS 队列中 因为调用 acquire 方法而被阻塞的 线程中 最先挂起的线程,即 头节点。
(二)tryReleaseShared:
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");
if (compareAndSetState(current, next))
return true;
}
}
可以看到,release 方法中每次只会对信号量值增加 1,而 (二)tryReleaseShared 中是无限循环,使用 CAS 保证了 release 方法对信号量递增 1 的原子性操作。
(6)release(int permits)
该方法与 不带参数 的 release 方法的不同之处在于,前者每次调用 会在信号量值 原来的基础上增加 permits ,而 不带参数的方法,每次增加 1。
public void release(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.releaseShared(permits);
}
可以看到,调用的是 releaseShared ,说明 信号量是线程共享的 ,信号量没有和固定线程绑定,多个线程可以同时使用 CAS 去更新信号量的值 而不会被阻塞。
🎭 总结:
Semaphore 完全可以达到 CountDownLatch 的效果 ,但是 Semaphore 的计数器是不可以自动重置的,不过可以变相改变 acquire 方法的参数,来重置。Semaphore 也是使用 AQS 实现的,并且 获取信号量时 有 公平策略 和 非公平策略。
四、总结
首先 ,CountDownLatch 通过计数器提供了更灵活的控制,只要检测到 计数器的值 为 0,就可以往下执行,这比使用 join 必须等待线程执行完毕后 主线程才能继续向下运行 灵活。
另外,CyclicBarrier 也可以达到 CountDownLatch 的效果,但是 CountDownLatch 在计数器值变为 0 后,就不能再被复用,而 CyclicBarrier 可以提供 reset 方法重置后使用。而且 CyclicBarrier 对于 同一个算法 但是输入参数不同的 类似情景 比较适用。
而 Semaphore 采用了信号量递增的策略,一开始并不需要关心同步的线程个数,等调用 acquire 方法时 再指定需要同步的个数,并且提供了获取信号量的 公平 与 非公平策略。
使用同步器吧😁 ,再日常开发中大大减少 wait、notify 来实现线程同步,使用同步器可以节省很多代码,而且保证正确性。