本次我们将讲述JUC包下的一些工具类
类 | 作用 |
---|---|
Semaphore | 限制线程的数量 |
Exchanger | 两个线程交换数据 |
CountDownLatch | 线程等待直到计数器减为0时开始工作 |
CyclicBarrier | 作用跟CountDownLatch类似,但是可以重复使用 |
Phaser | 增强的CyclicBarrier |
Semaphore(信号量)是用来控制同时访问特定资源的线程数量,因此他可以用于流量控制,特别是公用资源有限的应用场景,比如数据库连接假 如有一个需求,要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个线程 并发地读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这 时我们必须控制只有10个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。
我们可以在其构造函数中传入初始资源总数,以及是否使用公平的同步器,默认是非公平的。
// 默认情况下使用非公平
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
最主要的方法是acquire方法和release方法。
Semaphore内部有一个继承了AQS的同步器Sync,每次acquire()方法会申请一个permit,state就减1一次,直到许可证数量小于0则阻塞等待;
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted()) //调用前先检测该线程中断标志位,检测该线程在之前是否被中断过
throw new InterruptedException();
// 尝试获取共享资源锁,小于0则获取失败,tryAcquireShared方法由自定义同步器sync实现
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
// FairSync 公平锁的 tryAcquireShared 方法
protected int tryAcquireShared(int acquires) {
for (;;) { // 自旋的死循环操作方式
// 检查线程是否有阻塞队列:若有阻塞队列,说明共享资源的许可数量已经用完,返回-1进行入队操作
if (hasQueuedPredecessors())
return -1;
int available = getState(); // 获取锁资源的最新内存值
int remaining = available - acquires; // 计算得到剩下的许可数量remaining
if (remaining < 0 || // 若剩下的许可数量小于0,返回负数进入入队操作
compareAndSetState(available, remaining))
// 若共享资源大于或等于0,通过compareAndSetState操作占据最后一个共享资源
return remaining;
// 不管得到remaining后进入了何种逻辑,操作了之后再将remaining返回,上层会根据remaining的值进行判断是否需要入队操作
}
}
// NonfairSync 非公平锁的 tryAcquireShared 方法
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
// NonfairSync 非公平锁父类 Sync 类的 nonfairTryAcquireShared 方法
final int nonfairTryAcquireShared(int acquires) {
for (;;) { // 自旋的死循环操作方式
int available = getState(); // 获取最新许可证
int remaining = available - acquires; // 剩下的许可数量
if (remaining < 0 || // 若剩下的许可数量小于0,返回负数然后进入入队操作
compareAndSetState(available, remaining)) // 若共享资源大于或等于0,
//通过CAS操作占据最后一个共享资源
return remaining;
// 不管得到remaining后进入了何种逻辑,
//操作后再将remaining返回,上层会根据remaining的值进行判断是否需要入队操作
}
}
release()方法如下。释放许可的时候要保证唤醒后继结点,以此来保证线程释放他们所持有的信号量;
public void release() {
sync.releaseShared(1); // 释放一个许可资源
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { // 尝试释放共享锁资源,此方法由AQS的具体子类sync实现
doReleaseShared(); // 自旋操作,唤醒后继结点
return true;
}
return false;
}
// NonfairSync 和 FairSync 的父类 Sync 类的 tryReleaseShared 方法
protected final boolean tryReleaseShared(int releases) {
for (;;) { // 自旋的死循环操作方式
int current = getState(); // 获取最新的共享锁资源值
int next = current + releases; // 对许可数量进行加法操作
// int类型的state状态值可能溢出了,
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next)) //
return true; // 返回成功标志,告诉上层该线程已经释放了共享锁资源
}
}
本例我们希望在10个线程的情况下,最多有3个线程在工作。
public class Test {
static class MyThread extends Thread {
private int value;
private Semaphore semaphore;
public MyThread(int value, Semaphore semaphore) {
this.value = value;
this.semaphore = semaphore;
}
@Override
public void run() {
try {
semaphore.acquire();
System.out.println("线程" + value + "拿到许可证,还剩" + semaphore.availablePermits() +
"个许可证,还有" + semaphore.getQueueLength() + "个线程在等待");
Random random =new Random();
Thread.sleep(random.nextInt(1000));
semaphore.release();
System.out.println("线程" + value + "释放了许可证");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException{
Semaphore semaphore = new Semaphore(3);
for(int i = 0; i < 10; i++) {
new Thread(new MyThread(i, semaphore)).start();
}
}
}
输出:
Semaphore默认的acquire方法是会让线程进入等待队列,且会抛出中断异常。但它还有一些方法可以忽略中断或不进入阻塞队列:
// 忽略中断
public void acquireUninterruptibly()
public void acquireUninterruptibly(int permits)
// 不进入等待队列,底层使用CAS
public boolean tryAcquire
public boolean tryAcquire(int permits)
public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
throws InterruptedException
public boolean tryAcquire(long timeout, TimeUnit unit)
CountDownLatch是一种同步帮助,允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。CountDownLatch内部也定义了一个继承AQS的同步器Sync。
总结CountDownLatch的流程即为:
(1)管理一个大于0的计数器值
(2)每当一个线程执行一次countDown方法,计数器值减1,直到计数器值为0,那就释放队列中的索引等待线程。
举例来说:当我们进入一个游戏前,一般会有一些前置任务完成,比如说“加载地图数据”,“加载人物模型”,“加载背景音乐”。只有等这些任务都完成了,才能进入一个游戏。现在我们用CountDownLatch来模拟这个场景。
public class Test {
static class MyThread extends Thread {
private String taskName;
private CountDownLatch countDownLatch;
public MyThread(String taskName, CountDownLatch countDownLatch) {
this.taskName = taskName;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
try {
Random random =new Random();
Thread.sleep(random.nextInt(1000));
System.out.println(taskName + "任务完成");
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException{
CountDownLatch countDownLatch = new CountDownLatch(3);
//主任务
new Thread(new Runnable() {
@Override
public void run() {
try{
System.out.println("等待其他任务进行中");
System.out.println("还有" + countDownLatch.getCount() + "个待完成任务");
countDownLatch.await();
System.out.println("所有前置任务完成,开始主任务");
}catch (InterruptedException e) {
e.printStackTrace();
}
}}).start();
// 前置任务
new Thread(new MyThread("加载地图数据", countDownLatch)).start();
new Thread(new MyThread("加载人物模型", countDownLatch)).start();
new Thread(new MyThread("加载背景音乐", countDownLatch)).start();
}
}
输出:
等待其他任务进行中
还有3个待完成任务
加载地图数据任务完成
加载人物模型任务完成
加载背景音乐任务完成
所有前置任务完成,开始主任务
// 构造方法:
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count); //count赋值给了state
}
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
// 调用之前先检测该线程中断标志位,检测该线程在之前是否被中断过
if (Thread.interrupted())
throw new InterruptedException(); // 若被中断过,则抛出中断异常
// 尝试获取共享资源锁,小于0则获取失败,此方法由CountDownLatch的具体子类Sync实现
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg); // 将尝试获取锁资源的线程进行入队操作
}
//该方法判断计数器值是否为0
protected int tryAcquireShared(int acquires) {
// 计数器值与零比较判断,等于零则获取锁成功,否则获取锁失败
return (getState() == 0) ? 1 : -1;
}
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 按照给定的mode模式创建新的结点,模式有Node.EXCLUSIVE独占模式、Node.SHARED共享模式;
final Node node = addWaiter(Node.SHARED); // 创建共享模式的结点
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor(); // 获取结点的前驱结点
if (p == head) { //若前驱结点为head,则尝试获取锁,这是因为头结点可能会释放锁
int r = tryAcquireShared(arg);
/*
若r>=0,说明成功获取共享锁资源.
把当前node结点设置为头结点,该方法会调用doReleaseShared释放一下无用的结点
*/
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
/*
若第二个节点没有成功获取锁(r < 0),此时在第一次循环中,
该节点的waitStatus=0,因此被修改一次为SIGNAL状态。然后在第二次循环中。
shouldParkAfterFailedAcquire方法时,返回ture就是需要休眠,然后调用
park方法阻塞等待。
*/
if (shouldParkAfterFailedAcquire(p, node) && //根据前驱结点判断是否需要休息
// 阻塞操作,正常情况下,获取不到共享锁,代码就在该方法停止了,直到被唤醒
parkAndCheckInterrupt())
throw new InterruptedException();
// 被唤醒后,若发现parkAndCheckInterrupt()里面检测了被中断了的话,则抛出异常
}
} finally {
if (failed)
cancelAcquire(node);
}
}
public void countDown() {
sync.releaseShared(1); // 释放一个许可资源
}
//释放一个许可的方法
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { // 尝试释放共享锁资源,此方法由Sync实现
doReleaseShared(); // 自旋操作,唤醒后继结点
return true; // 返回true表明所有线程已释放
}
return false; // 返回false表明目前还没释放完全,只要计数器值不为零的话,那么都会返回false
}
//该方法尝试将计数器减1,该方法由CountDownLatch 的静态内部类 Sync 类实现
protected boolean tryReleaseShared(int releases) {
for (;;) {
// 获取最新的计数器值,若为0,表示已通过CAS操作减至零,无需做什么,返回false
int c = getState();
if (c == 0)
return false;
int nextc = c-1; // 计数器值减1操作
if (compareAndSetState(c, nextc)) // 通过CAS比较,顺利情况下设置成功返回true
return nextc == 0;
//若CAS操作成功且计数值state被减成了0,则需要释放所有等待的线程队列
// 若CAS失败,则在下一次循环查看是否已经被其他线程处理了
}
}
//该方法尝试将头结点置为空,并唤醒后继节点
private void doReleaseShared() {
for (;;) {
Node h = head; // 每次都是取出队列的头结点
if (h != null && h != tail) { // 若头结点不为空且也不是队尾结点
int ws = h.waitStatus; //获取头结点的waitStatus状态值
if (ws == Node.SIGNAL) { // 若头结点是SIGNAL状态则意味着头结点的后继结点需要被唤醒了
//通过CAS尝试设置头结点的状态为空状态,失败的话,则继续循环,这是因为可能有其他线程也在释放。
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
unparkSuccessor(h); // 唤醒头结点的后继结点
}
//若头结点为空状态,则把其改为PROPAGATE状态,失败是因为其他线程改动过,因此再次循环处理
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
// 若头结点没有发生什么变化,则说明上述设置已经完成
// 若发生了变化,可能是操作过程中头结点有了新增,那么则必须进行重试,以保证唤醒动作可以延续传递
if (h == head)
break;
}
}
CyclicBarrier允许多个线程相互等待,即多个线程到达同步点时被阻塞,直到最后一个线程到达同步点时栅栏才会被打开。之前介绍的CountDownLatch一旦计数值等于0后,就不能再重新设置,即只有1次屏障的作用;而CyclicBarrier不仅拥有CountDownLatch的所有功能,还可以使用reset()方法重置屏障。
同样是CountDownLatch中的例子,不过我们等待需要完成的前置任务换成了三个关卡,每个关卡的前置任务都完成了,就可以进入游戏。
public class Test {
static class MyThread extends Thread {
private String taskName;
private CyclicBarrier cyclicBarrier;
public MyThread(String taskName, CyclicBarrier cyclicBarrier) {
this.taskName = taskName;
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
//总共3个关卡
for(int i = 1; i < 4; i++) {
try {
Random random =new Random();
Thread.sleep(random.nextInt(1000));
System.out.print(Thread.currentThread().getName() + ":");
System.out.println("关卡" + i + "的" + taskName + "任务完成");
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
cyclicBarrier.reset(); //重置屏障
}
}
}
public static void main(String[] args) throws InterruptedException{
Thread anoTask = new Thread(new Runnable() {
//主任务
@Override
public void run() {
System.out.print(Thread.currentThread().getName());
System.out.println("本卡关所有任务均已完成,开始进入游戏");
}
});
anoTask.setName("gg");
CyclicBarrier cyclicBarrier = new CyclicBarrier(3, anoTask);
// 前置任务
Thread t1 = new Thread(new MyThread("加载地图数据", cyclicBarrier));
t1.setName("t1");
t1.start();
Thread t2 = new Thread(new MyThread("加载人物模型", cyclicBarrier));
t2.setName("t2");
t2.start();
Thread t3 = new Thread(new MyThread("加载背景音乐", cyclicBarrier));
t3.setName("t3");
t3.start();
}
}
输出:
t1:关卡1的加载地图数据任务完成
t3:关卡1的加载背景音乐任务完成
t2:关卡1的加载人物模型任务完成
t2本卡关所有任务均已完成,开始进入游戏
t1:关卡2的加载地图数据任务完成
t2:关卡2的加载人物模型任务完成
t3:关卡2的加载背景音乐任务完成
t3本卡关所有任务均已完成,开始进入游戏
t3:关卡3的加载背景音乐任务完成
t2:关卡3的加载人物模型任务完成
t1:关卡3的加载地图数据任务完成
t1本卡关所有任务均已完成,开始进入游戏
CyclicBarrier没有分为await()
和countDown()
,它只有单独的一个await()方法。当调用await()方法的线程数量等于构造方法中传入的任务总量3,就代表达到屏障了。CyclicBarrier允许我们在达到屏障时执行一个任务,可以在构造方法传入一个Runnable类型的对象。上例中就是在达到屏障(完成三个前置任务)后,选择一个前置任务线程来输出“本卡关所有任务均已完成,开始进入游戏”
// 构造函数一
public CyclicBarrier(int parties) {
this(parties, null);
}
// 构造函数二
//parties参数为支持参与的最多线程,当最后一个阻塞线程被释放后,它有机会执行任务barrierAction
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe);
}
}
/*
dowait方法是CyclicBarrier实现阻塞等待的核心方法,当await方法被调用时阻塞等待被Condition
的一个队列trip维护着.
线程从await跳出来时,正常情况下一般都是由于发送了信号量,阻塞被解除,那么Condition的等待队列
将会被转移至AQS的等待队列; 然后一个逐渐锁释放,最后CyclicBarrier也处于了初始值状态,供下次调用使用;
因此CyclicBarrier每用完一套整个流程,又会回到初始状态值,又可以被其他地方当做新创建的对象一样
来使用,所以才成为循环栅栏;
*/
private int dowait(boolean timed, long nanos)throws InterruptedException,BrokenBarrierException, TimeoutException {
final ReentrantLock lock = this.lock; // 获取独占锁
lock.lock(); // 通过lock其父类AQS的CLH队列阻塞在此,但是为啥又会继续往下进入临界区执行try方法,其原因就是trip.await()这句代码
try {
final Generation g = generation;
if (g.broken) //平衡被打破,则其他所有的线程都会抛出异常,
throw new BrokenBarrierException();
//检测线程是否在其他地方被中断过,若任何一个线程被中断过,则打破平衡,并设置打破平衡的标志,
//还原初始状态值,然后再唤醒所有被阻塞的线程,
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
//count表示还有多少个线程还在lock阻塞队列中
int index = --count;
if (index == 0) { // 当count值降为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 {
// 若不需要使用超时等待信号量的话,那么下面就直接调用trip.await()进入阻塞等待
if (!timed)
// 正常情况下,代码执行到此就不动了,该方法内部已经调用了park方法导致线程阻塞等待
trip.await();
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos); // 在指定时间内等待信号量
} catch (InterruptedException ie) { // 若在阻塞等待期间由于被中断了
// 如果还没更换标识位,
//并且平衡标志位还为false的话,则继续打破平衡并且抛出中断异常
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
Thread.currentThread().interrupt();
}
}
if (g.broken) // ,若平衡被打破,则其他所有的线程都会抛出异常
throw new BrokenBarrierException();
if (g != generation) // 若已经被改朝换代了,那么则直接返回index值
return index;
// 若设置了超时标志,并且不管是传入的nanos值也好还是通过等待后返回的nanos也好,
//只要小于或等于零都会打破平衡
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
//打破平衡,并设置打破平衡的标志,然后再唤醒所有被阻塞的线程;
private void breakBarrier() {
generation.broken = true; // 设置打破平衡的标志
count = parties; // 重新还原count为初始值
trip.signalAll(); // 发送信号量,唤醒所有Condition中的等待队列
}
//唤醒所有在Condition中等待的队列,然后还原初始状态值,
//并且重新换掉generation的引用,改朝换代,为下一轮操作做准备;
private void nextGeneration() {
// signal completion of last generation
trip.signalAll();
// set up next generation
count = parties;
generation = new Generation();
}
https://blog.csdn.net/YLIMH_HMILY/article/details/79521610