java.util.concurrent包 — 并发工具大集合
一、并发工具类介绍
通常我们所说的并发工具包,即为 java.util.concurrent 包,内部包含了 java 并发的各种工具类,若合理的使用能够帮助我们快速完成一些功能。
以下将介绍主要的工具类,以及内部主要的使用方法和原理,并提供代码示例
工具类名 | 作用说明 | 作用简述 |
---|---|---|
CountDownLatch | jdk1.5 引入;是一个同步计数器,初始化的时候 传入需要计数的线程等待数,可以是需要等待执行完成的线程数 | 等待其他线程完成,并当计数器减为0 后在向下执行 |
Semaphore | 简称 “信号量”,在jdk1.5 引入;可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。 | 用于控制同时访问某个资源的线程数量,通常用于”限流“ |
Exchanger | 是一个线程间交换数据的工具类。它允许两个线程在彼此之间交换对象,并提供了一个方法:exchange() 。调用 exchange() 方法会阻塞当前线程,直到另一个线程调用了相同的方法。 | 两个线程交换数据 |
CyclicBarrier | CyclicBarrier 是一个栅栏,用于控制多个线程之间的同步。它通过一个指定的计数器来实现,并提供了await() 和reset() 两个方法。await() 方法用于阻塞当前线程,直到所有线程都到达了栅栏位置,reset() 方法用于重置计数器。 | 作用跟CountDownLatch类似,但是可以重复使用 |
Phaser | Phaser 是一个更加高级的栅栏,它可以用于控制多个阶段的线程之间的同步。它通过一个指定的阶段数和参与者数量来实现,并提供了多个方法:arrive() , awaitAdvance() , arriveAndAwaitAdvance() 等。 | 增强的CyclicBarrier |
二、工具类的使用
1、CountDownLatch
在 jdk1.5 时被引入的一个同步工具类,常常被成为 ”倒计数器“,允许一个或多个线程一直等待,直到其他线程的操作都执行完成之后再继续往下执行。
在多线程的业务场景中,有时需要等待其他多个线程完成任务之后,主线程才能继续往下执行业务功能,在
java
并发工具类中提供的CountDownLatch
恰好能满足此业务场景。
1.1、 CountDownLatch
类内部的源码:
public class CountDownLatch {
private final CountDownLatch.Sync sync;
public CountDownLatch(int var1) {
if (var1 < 0) {
throw new IllegalArgumentException("count < 0");
} else {
this.sync = new CountDownLatch.Sync(var1);
}
}
public void await() throws InterruptedException {
this.sync.acquireSharedInterruptibly(1);
}
public boolean await(long var1, TimeUnit var3) throws InterruptedException {
return this.sync.tryAcquireSharedNanos(1, var3.toNanos(var1));
}
public void countDown() {
this.sync.releaseShared(1);
}
public long getCount() {
return (long)this.sync.getCount();
}
public String toString() {
return super.toString() + "[Count = " + this.sync.getCount() + "]";
}
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int var1) {
this.setState(var1);
}
int getCount() {
return this.getState();
}
protected int tryAcquireShared(int var1) {
return this.getState() == 0 ? 1 : -1;
}
protected boolean tryReleaseShared(int var1) {
int var2;
int var3;
do {
var2 = this.getState();
if (var2 == 0) {
return false;
}
var3 = var2 - 1;
} while(!this.compareAndSetState(var2, var3));
return var3 == 0;
}
}
}
1.2、内部方法解释说明:
CountDownLatch(int var1)
:构造方法,内部会传入一个 int 类型的整数,用来指定计数的初始值;计数是主线程必须等待其他线程完成任务的数量
await()
:调用此方法的线程将被阻塞,直到构造方法传入的 计数 达到 零
,才可以继续向下执行。
await(long var1, TimeUnit var3)
:可指定最长等待时间,如果在这段时间内计数没有达到零,则线程将继续执行。
countDown()
:每次调用此方法都会使计数减少1;当计数减少到0时,等待的所有线程都将继续执行。
getCount()
:获取当前计数的值
注意
:构造器中的计数值(count)实际上就是需要等待线程的数量,这个值只能被设置一次,因CountDownLatch
没有提供任何机制去重新设置这个计数值。
1.3、代码示例
为了让大家更好的理解并使用 CountDownLatch
,我将采用运动员跑步比赛的方式来写一份代码示例:
/**
* @author : gaogao
* @version 1.0
*/
@Slf4j
@SpringBootTest
public class PublicWechatApplicationTests {
private ExecutorService executor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), Runtime.getRuntime().availableProcessors() << 2, 1200L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(20000), new ThreadFactoryBuilder().setNameFormat("- 高高手动创建的线程池-%d").build(), new ThreadPoolExecutor.AbortPolicy());
@Test
public void execute() throws InterruptedException {
// 1名裁判
CountDownLatch referee = new CountDownLatch(1);
// 5名运动员
CountDownLatch sportsman = new CountDownLatch(5);
for (int i = 1; i < 6; i++) {
int finalI = i;
log.info(" 线程 {} 执行任务:{} 号运动员选手波等待裁判员响哨!!!", Thread.currentThread().getName(), finalI);
executor.execute(() -> {
try {
referee.await();
log.info(" 线程 {} 执行任务:{} 号运动员正在冲刺中!!!", Thread.currentThread().getName(), finalI);
// ......过了一段时间
log.info(" 线程 {} 执行任务:{} 号运动员抵达终点!!!", Thread.currentThread().getName(), finalI);
} catch (Exception e) {
log.error("任务执行异常:", e);
} finally {
sportsman.countDown();
}
});
}
log.info("裁判员鸣枪发号施令~~~");
referee.countDown();
sportsman.await();
log.info("所有运动员抵达终点,比赛顺利结束!!!");
}
/**
* 对上述代码的解释说明:
* 在本场运动员比赛过程中,共有5名运动员选手和一名裁判。
* 步骤1 :5名运动员选手站在起跑线上
* 步骤2:选手们等待裁判鸣枪,阻塞线程等待
* 步骤3:裁判鸣枪,发号施令,referee 减1 操作; 此时步骤2 referee 进行的计数为零,线程阻塞结束,所有运动员开始跑,跑完结束后在 finally 的步骤 2-1 进行减1操作,告诉裁判当前运动员已跑完
* 步骤4 所有运动员选手还没有跑完,开始阻塞主线程等待; 直到所有运动员都跑完, 进行 步骤 2-1 操作,当前获取计数器的计数为零时,开始向下执行
* 最终 所有运动员比赛结束
*/
}
输出结果:
2023-12-22 09:45:07.587 INFO 26692 --- [ main] c.p.PublicWechatApplicationTests : 线程 main 执行任务:1 号运动员选手波等待裁判员响哨!!!
2023-12-22 09:45:07.589 INFO 26692 --- [ main] c.p.PublicWechatApplicationTests : 线程 main 执行任务:2 号运动员选手波等待裁判员响哨!!!
2023-12-22 09:45:07.590 INFO 26692 --- [ main] c.p.PublicWechatApplicationTests : 线程 main 执行任务:3 号运动员选手波等待裁判员响哨!!!
2023-12-22 09:45:07.590 INFO 26692 --- [ main] c.p.PublicWechatApplicationTests : 线程 main 执行任务:4 号运动员选手波等待裁判员响哨!!!
2023-12-22 09:45:07.590 INFO 26692 --- [ main] c.p.PublicWechatApplicationTests : 线程 main 执行任务:5 号运动员选手波等待裁判员响哨!!!
2023-12-22 09:45:07.590 INFO 26692 --- [ main] c.p.PublicWechatApplicationTests : 裁判员鸣枪发号施令~~~
2023-12-22 09:45:07.590 INFO 26692 --- [ - 高高手动创建的线程池-1] c.p.PublicWechatApplicationTests : 线程 - 高高手动创建的线程池-1 执行任务:2 号运动员正在冲刺中!!!
2023-12-22 09:45:07.590 INFO 26692 --- [ - 高高手动创建的线程池-2] c.p.PublicWechatApplicationTests : 线程 - 高高手动创建的线程池-2 执行任务:3 号运动员正在冲刺中!!!
2023-12-22 09:45:07.590 INFO 26692 --- [ - 高高手动创建的线程池-4] c.p.PublicWechatApplicationTests : 线程 - 高高手动创建的线程池-4 执行任务:5 号运动员正在冲刺中!!!
2023-12-22 09:45:07.590 INFO 26692 --- [ - 高高手动创建的线程池-0] c.p.PublicWechatApplicationTests : 线程 - 高高手动创建的线程池-0 执行任务:1 号运动员正在冲刺中!!!
2023-12-22 09:45:07.590 INFO 26692 --- [ - 高高手动创建的线程池-1] c.p.PublicWechatApplicationTests : 线程 - 高高手动创建的线程池-1 执行任务:2 号运动员抵达终点!!!
2023-12-22 09:45:07.590 INFO 26692 --- [ - 高高手动创建的线程池-2] c.p.PublicWechatApplicationTests : 线程 - 高高手动创建的线程池-2 执行任务:3 号运动员抵达终点!!!
2023-12-22 09:45:07.590 INFO 26692 --- [ - 高高手动创建的线程池-3] c.p.PublicWechatApplicationTests : 线程 - 高高手动创建的线程池-3 执行任务:4 号运动员正在冲刺中!!!
2023-12-22 09:45:07.590 INFO 26692 --- [ - 高高手动创建的线程池-4] c.p.PublicWechatApplicationTests : 线程 - 高高手动创建的线程池-4 执行任务:5 号运动员抵达终点!!!
2023-12-22 09:45:07.590 INFO 26692 --- [ - 高高手动创建的线程池-0] c.p.PublicWechatApplicationTests : 线程 - 高高手动创建的线程池-0 执行任务:1 号运动员抵达终点!!!
2023-12-22 09:45:07.590 INFO 26692 --- [ - 高高手动创建的线程池-3] c.p.PublicWechatApplicationTests : 线程 - 高高手动创建的线程池-3 执行任务:4 号运动员抵达终点!!!
2023-12-22 09:45:07.590 INFO 26692 --- [ main] c.p.PublicWechatApplicationTests : 所有运动员抵达终点,比赛顺利结束!!!
2、Semaphore
Semaphore
(通常称为信号量
)是一个计数的同步工具,内部维护了一个许可集合,用于限制访问某个资源的线程数量。
Semaphore
最主要是通过acquire()
方法获取许可,通过release()
方法释放许,通过availablePermits()
方法获取当前剩余可使用的许可数量。如果许可为0
,那么线程进行阻塞;
2.1、常见方法解释说明
Semaphore(int var1)
:非公平模式创建 var1
个许可;
Semaphore(int var1, boolean var2)
:可以指定是否公平模式创建 var1
个许可;true
为公平模式创建;
acquire()
:尝试获取一个许可,如果没有许可可用则当前线程阻塞,直到许可变得可用才继续向下执行;可被中断停止等待;
acquire(int var1)
:和上述方法相同,只是这里尝试进行获取 var1
数量的许可;
acquireUninterruptibly()
:尝试获取一个许可,不可中断;
acquireUninterruptibly(int var1)
:尝试获取 var1
数量的许可,不可中断;
tryAcquire()
:尝试获取一个许可,如果许可立即可用,则获取许可并返回true,否则返回false;
示例:
boolean success = semaphore.tryAcquire();
tryAcquire(int var1)
:和上述方法相同;尝试获取 var1
个许可,获取不到则直接返回失败;
tryAcquire(int var1, long timeout, TimeUnit unit)
:尝试在timeout
时间内获取var1
个许可,超时则返回false,可被中断;
tryAcquire(long timeout, TimeUnit unit)
:尝试在 timeout
时间内获取1个许可,超时则返回false,可被中断;
release()
:释放一个许可;
release(int var1)
:释放 var1
个许可;
availablePermits()
: 返回当前可用的许可数量。
示例:
int i = semaphore.availablePermits();
Semaphore适用于控制资源的并发访问数量,例如限制同时访问某个文件的线程数量。公平策略保证等待时间最长的线程首先获得许可。
2.2、代码示例
为了让大家更好的理解并使用 Semaphore
,我将使用饭店就餐的场景来写一份代码示例:
/**
* @author : gaogao
* @version 1.0
*/
@Slf4j
@SpringBootTest
public class SemaphoreTests {
// 阻塞队列
private static BlockingQueue<String> queue = new LinkedBlockingQueue<>(6);
static {
// 初始化 六个 桌位
queue.offer("1号桌");
queue.offer("2号桌");
queue.offer("3号桌");
queue.offer("4号桌");
queue.offer("5号桌");
queue.offer("6号桌");
}
// 产生随机数
private static Random random = new Random();
public static void main(String[] args) {
// 初始化线程的数量,即共有十二桌人进行排队就餐
int threadCount = 12;
Semaphore semaphore = new Semaphore(6);
for (int i = 0; i < threadCount; i++) {
final int peopleIndex = i;
new Thread(() -> {
execute(semaphore, peopleIndex);
}).start();
}
}
/**
* 限流,限制客人同时就餐数量
* @param semaphore
* @param peopleIndex
*/
public static void execute(Semaphore semaphore, int peopleIndex) {
try {
// 获取许可令牌,如果没拿到就等待,拿到则许可令牌数量减 1
semaphore.acquire();
// 获取桌位
String take = queue.take();
log.info("客人:{},开始在 {} 就餐,当前剩余餐桌数:{}", peopleIndex, take, semaphore.availablePermits());
// 此处用睡眠时间 代替顾客实际用餐时间
int sleepTime = (random.nextInt(5) + 1) * 10;
Thread.sleep(sleepTime * 1000);
// 桌位的客人离桌,餐桌回归空闲
queue.offer(take);
log.info("客人:{},在 {} 就餐就餐完毕,正在退场...,就餐时长为:{} 秒", peopleIndex, take, sleepTime);
// 释放许可,许可令牌数量 加1
semaphore.release();
} catch (Exception e) {
log.error("执行异常:", e);
}
}
}
输出结果:
11:34:17.278 [Thread-0] INFO com.public_wechat.SemaphoreTests - 客人:0,开始在 1号桌 就餐,当前剩余餐桌数:4
11:34:17.278 [Thread-2] INFO com.public_wechat.SemaphoreTests - 客人:2,开始在 3号桌 就餐,当前剩余餐桌数:3
11:34:17.278 [Thread-4] INFO com.public_wechat.SemaphoreTests - 客人:4,开始在 5号桌 就餐,当前剩余餐桌数:1
11:34:17.278 [Thread-5] INFO com.public_wechat.SemaphoreTests - 客人:5,开始在 6号桌 就餐,当前剩余餐桌数:0
11:34:17.278 [Thread-1] INFO com.public_wechat.SemaphoreTests - 客人:1,开始在 2号桌 就餐,当前剩余餐桌数:4
11:34:17.278 [Thread-3] INFO com.public_wechat.SemaphoreTests - 客人:3,开始在 4号桌 就餐,当前剩余餐桌数:2
11:34:57.292 [Thread-3] INFO com.public_wechat.SemaphoreTests - 客人:3,在 4号桌 就餐就餐完毕,正在退场...,就餐时长为:40 秒
11:34:57.293 [Thread-6] INFO com.public_wechat.SemaphoreTests - 客人:6,开始在 4号桌 就餐,当前剩余餐桌数:0
11:35:07.285 [Thread-4] INFO com.public_wechat.SemaphoreTests - 客人:4,在 5号桌 就餐就餐完毕,正在退场...,就餐时长为:50 秒
11:35:07.285 [Thread-1] INFO com.public_wechat.SemaphoreTests - 客人:1,在 2号桌 就餐就餐完毕,正在退场...,就餐时长为:50 秒
11:35:07.285 [Thread-0] INFO com.public_wechat.SemaphoreTests - 客人:0,在 1号桌 就餐就餐完毕,正在退场...,就餐时长为:50 秒
11:35:07.285 [Thread-5] INFO com.public_wechat.SemaphoreTests - 客人:5,在 6号桌 就餐就餐完毕,正在退场...,就餐时长为:50 秒
11:35:07.285 [Thread-7] INFO com.public_wechat.SemaphoreTests - 客人:7,开始在 3号桌 就餐,当前剩余餐桌数:2
11:35:07.285 [Thread-2] INFO com.public_wechat.SemaphoreTests - 客人:2,在 3号桌 就餐就餐完毕,正在退场...,就餐时长为:50 秒
11:35:07.285 [Thread-8] INFO com.public_wechat.SemaphoreTests - 客人:8,开始在 1号桌 就餐,当前剩余餐桌数:3
11:35:07.285 [Thread-9] INFO com.public_wechat.SemaphoreTests - 客人:9,开始在 5号桌 就餐,当前剩余餐桌数:1
11:35:07.285 [Thread-10] INFO com.public_wechat.SemaphoreTests - 客人:10,开始在 6号桌 就餐,当前剩余餐桌数:0
11:35:07.285 [Thread-11] INFO com.public_wechat.SemaphoreTests - 客人:11,开始在 2号桌 就餐,当前剩余餐桌数:0
11:35:37.298 [Thread-7] INFO com.public_wechat.SemaphoreTests - 客人:7,在 3号桌 就餐就餐完毕,正在退场...,就餐时长为:30 秒
11:35:37.298 [Thread-8] INFO com.public_wechat.SemaphoreTests - 客人:8,在 1号桌 就餐就餐完毕,正在退场...,就餐时长为:30 秒
11:35:47.293 [Thread-11] INFO com.public_wechat.SemaphoreTests - 客人:11,在 2号桌 就餐就餐完毕,正在退场...,就餐时长为:40 秒
11:35:47.293 [Thread-9] INFO com.public_wechat.SemaphoreTests - 客人:9,在 5号桌 就餐就餐完毕,正在退场...,就餐时长为:40 秒
11:35:47.293 [Thread-10] INFO com.public_wechat.SemaphoreTests - 客人:10,在 6号桌 就餐就餐完毕,正在退场...,就餐时长为:40 秒
11:35:47.308 [Thread-6] INFO com.public_wechat.SemaphoreTests - 客人:6,在 4号桌 就餐就餐完毕,正在退场...,就餐时长为:50 秒
Semaphore内部有一个继承了AQS的同步器Sync,重写了tryAcquireShared方法。在这个方法内会去尝试获取资源。如果获取失败,就会返回一个负数(代表尝试获取资源失败)。然后当前线程就会进入AQS的等待队列
3、Exchanger
3.1、使用场景
Exchanger
的主要使用场景是在两个线程之间安全地交换数据。它可以用于以下情况:
-
线程间的数据传递:两个线程之间需要传递数据,并且需要确保数据的完整性和一致性。
-
生产者-消费者模式:一个线程用于生成数据,另一个线程用于消费数据,
Exchanger
用于在它们之间进行数据交换。
3.2、内部源码方法说明
exchange(V x)
:该方法用于将数据 x 交换给另一个线程,并返回另一个线程传递过来的数据。如果当前线程先调用 exchange()
方法,它将会阻塞等待另一个线程也调用 exchange()
方法,然后交换数据。
exchange(V x, long timeout, TimeUnit unit)
:该方法与上述方法类似,但是在超时时间内如果没有另一个线程调用 exchange()
方法,则当前线程将继续执行,并返回一个默认值。
3.3、执行原理
Exchanger
的执行原理如下:
- 当一个线程调用
exchange()
方法时,它会进入等待状态,直到另一个线程也调用exchange()
方法。 - 当第二个线程调用
exchange()
方法时,两个线程会交换数据,并返回对方传递过来的数据。 - 如果有多个线程同时调用
exchange()
方法,它们将按照先后顺序进行交换。
Exchanger
使用一个双端队列来管理等待线程,当一个线程调用 exchange()
方法时,它会加入到队列中并等待另一个线程的到来。当另一个线程也调用 exchange()
方法时,两个线程会交换数据,然后被移出队列,继续执行。
3.4、代码示例
/**
* @author : gaogao
* @version 1.0
*/
@Slf4j
public class ExchangerComplexExample {
public static void main(String[] args) {
Exchanger<String> exchanger = new Exchanger<>();
Thread producerThread = new Thread(() -> {
try {
String[] messages = {"Message 1 from Producer", "Message 2 from Producer", "Message 3 from Producer"};
for (String message : messages) {
// log.info("Producer is producing data: " + message);
String receivedData = exchanger.exchange(message);
log.info("Producer received data from Consumer: " + receivedData);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread consumerThread = new Thread(() -> {
try {
String[] messages = {"Message 1 from Consumer", "Message 2 from Consumer", "Message 3 from Consumer"};
for (String message : messages) {
// log.info("Consumer is producing data: " + message);
String receivedData = exchanger.exchange(message);
log.info("Consumer received data from Producer: " + receivedData);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producerThread.start();
consumerThread.start();
}
}
3.5、疑问
Exchanger只能是两个线程交换数据吗?那三个调用同一个实例的exchange方法会发生什么呢?
答:只有前两个线程会交换数据,第三个线程会进入阻塞状态。
注意:
exchange
是可以重复使用的。也就是说:两个线程可以使用Exchanger
在内存中不断地再交换数据
4、CyclicBarrier
4.1 简介
CyclicBarrier,是 JDK1.5 的 java.util.concurrent (JUC) 并发包中提供的一个并发工具类。是一个同步工具,允许一组线程互相等待,直到所有线程都达到一个公共障碍点(Barrier),一旦最后一个线程到达屏障点,屏障就会打开,所有的等待线程都将继续执行。
前面提到的
CountDownLatch
一旦计数值count
被减少到0
后,就不能再重新设置了,它只能起一次“屏障”的作用。而CyclicBarrier
拥有CountDownLatch
的所有功能,还可以使用reset()
方法重置屏障。
4.3、类内部属性
public class CyclicBarrier {
// 可重入锁
private final ReentrantLock lock;
// 条件队列
private final Condition trip;
// 参与的线程数量
private final int parties;
// 由最后一个进入 barrier 的线程执行的操作
private final Runnable barrierCommand;
private CyclicBarrier.Generation generation;
// 正在等待进入屏障的线程数量
private int count;
//....
}
4.3、常见方法解释说明
CyclicBarrier(int var1)
:指定 CyclicBarrier
的线程数量,不设置执行动作
CyclicBarrier(int var1, Runnable var2)
:指定CyclicBarrier
的线程数量,并指定一个Runnable
线程,在所有线程都进入屏障后执行此线程,该执行动作由最后一个进行屏障的线程执行。
await()
:当线程调用此方法时告知CyclicBarrier
已经到达屏障点,并进入阻塞状态,直到其他线程执行完成(简述:阻塞当前线程,直到其他线程全部执行完成)
await(long var1, TimeUnit var3)
:和上述方法相同,额外可以指定等待最长时间,如果达到时间屏障还没打开,则抛出TimeoutException
reset()
:将屏障重置为初始状态;如果当前有线程正在临界点等待的话,将抛出BrokenBarrierException。
getNumberWaiting()
:返回当前在屏障处等待的线程数。
CyclicBarrier
在并发编程中常用于协调多个线程,以确保它们在继续执行下一阶段任务之前都完成当前阶段任务。和CountDownLatch
不同的是,CyclicBarrier
可以重用,所以它被称为循环(Cyclic
)屏障(Barrier
)
4.4、代码示例
多线程读取多个 Excel 文件,并计算每个文件中每个 sheet 页的数据和并输出,在执行工作中输出该 Excel 文件内所有 sheet 页的数据和,以及所有文件的所有sheet页的数据和。上述业务场景代码示例如下:
/**
* @author : gaogao
* @version 1.0
* @date : 2023-12-22 14:32
*/
@Slf4j
@SpringBootTest
public class CyclicBarrierTests {
private static ExecutorService executor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), Runtime.getRuntime().availableProcessors() << 2, 1200L,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(20000), new ThreadFactoryBuilder().setNameFormat("- 高高手动创建的线程池-%d").build(), new ThreadPoolExecutor.AbortPolicy());
// 读取本地的文件
private static final String[] FILE_PATHS = {"C:\\Users\\97697\\Desktop\\新建 XLSX 工作表.xlsx", "C:\\Users\\97697\\Desktop\\新建 XLSX 工作表 - 副本.xlsx"};
// 内部类
private static final SheetSumWrapper sheetSumWrapper = new SheetSumWrapper();
private static final CyclicBarrier barrier = new CyclicBarrier(getTotalSheetCount(), () -> {
double totalSum = 0;
for (Map.Entry<String, Map<String, Double>> entry : sheetSumWrapper.getSheetSums().entrySet()) {
String key = entry.getKey();
Map<String, Double> value = entry.getValue();
int sum = value.values().stream().mapToInt(Double::intValue).sum();
log.info("文件:{},共计 {} sheet页面,数据和为:{}", key, value.size(), sum);
totalSum += sum;
}
log.info("所有excel文件已读取完数据,并计算完毕!,总文件数据和为:{}", totalSum);
});
public static void main(String[] args) {
for (String filePath : FILE_PATHS) {
File file = new File(filePath);
if (!file.exists()) {
log.info("File does not exist: {}", filePath);
continue;
}
try (Workbook workbook = new XSSFWorkbook(file)) {
int sheetCount = workbook.getNumberOfSheets();
// 读取每一个sheet页
for (int i = 0; i < sheetCount; i++) {
int sheetIndex = i;
executor.execute(() -> readSheetAndComputeSum(file.getName(), workbook, sheetIndex));
}
} catch (IOException | InvalidFormatException e) {
e.printStackTrace();
}
}
log.info("庆祝顺利统计excel数据和完毕!!!");
}
public static void readSheetAndComputeSum(String fileName, Workbook workbook, int sheetIndex) {
Sheet sheet = workbook.getSheetAt(sheetIndex);
double sum = readSheet(sheet);
log.info("文件名:{},sheet页码:{}, 当前sheet内的数据和:{}", fileName, workbook.getSheetName(sheetIndex), sum);
try {
sheetSumWrapper.setSheetSum(fileName, workbook.getSheetName(sheetIndex), sum);
// 等待其他线程读取完数据
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
public static double readSheet(Sheet sheet) {
double sum = 0;
for (Row row : sheet) {
// 假设需要计算第二列的数据
Cell cell = row.getCell(1);
// if (cell != null && cell.getCellType() == nu) {
if (cell != null) {
sum += cell.getNumericCellValue();
}
}
return sum;
}
private static int getTotalSheetCount() {
int totalSheetCount = 0;
for (String filePath : FILE_PATHS) {
try (Workbook workbook = new XSSFWorkbook(new File(filePath))) {
totalSheetCount += workbook.getNumberOfSheets();
} catch (IOException | InvalidFormatException e) {
e.printStackTrace();
}
}
return totalSheetCount;
}
}
实体类,用于封装结果集:
/**
* @author : gaogao
* @version 1.0
* @date : 2023-12-25 18:01
*/
@Data
public class SheetSumWrapper {
private Map<String, Map<String, Double>> sheetSums = Maps.newHashMap();
public synchronized Map<String, Map<String, Double>> getSheetSums() {
return sheetSums;
}
public synchronized void setSheetSum(String fileName, String sheetName, double sum) {
Map<String, Double> doubleMap = sheetSums.computeIfAbsent(fileName, k -> new HashMap<>());
doubleMap.merge(sheetName, sum, Double::sum);
}
}
输出结果:
18:31:41.900 [- 高高手动创建的线程池-0] INFO com.public_wechat.CyclicBarrierTests - 文件名:新建 XLSX 工作表.xlsx,sheet页码:Sheet1, 当前sheet内的数据和:20.0
18:31:41.900 [- 高高手动创建的线程池-1] INFO com.public_wechat.CyclicBarrierTests - 文件名:新建 XLSX 工作表.xlsx,sheet页码:Sheet2, 当前sheet内的数据和:20.0
18:31:41.945 [- 高高手动创建的线程池-2] INFO com.public_wechat.CyclicBarrierTests - 文件名:新建 XLSX 工作表 - 副本.xlsx,sheet页码:Sheet1, 当前sheet内的数据和:30.0
18:31:41.948 [- 高高手动创建的线程池-3] INFO com.public_wechat.CyclicBarrierTests - 文件名:新建 XLSX 工作表 - 副本.xlsx,sheet页码:Sheet2, 当前sheet内的数据和:40.0
18:31:41.948 [- 高高手动创建的线程池-4] INFO com.public_wechat.CyclicBarrierTests - 文件名:新建 XLSX 工作表 - 副本.xlsx,sheet页码:Sheet3, 当前sheet内的数据和:0.0
18:31:41.954 [- 高高手动创建的线程池-4] INFO com.public_wechat.CyclicBarrierTests - 文件:新建 XLSX 工作表 - 副本.xlsx,共计 3 sheet页面,数据和为:70
18:31:41.954 [- 高高手动创建的线程池-4] INFO com.public_wechat.CyclicBarrierTests - 文件:新建 XLSX 工作表.xlsx,共计 2 sheet页面,数据和为:40
18:31:41.954 [- 高高手动创建的线程池-4] INFO com.public_wechat.CyclicBarrierTests - 所有excel文件已读取完数据,并计算完毕!,总文件数据和为:110.0
18:31:41.969 [main] INFO com.public_wechat.CyclicBarrierTests - 庆祝顺利统计excel数据和完毕!!!
对上述代码示例的解释说明:
-
使用
CyclicBarrier
来实现多线程同时读取完所有 sheet 页后,再计算每个 sheet 页的数据和,最终输出所有 Excel 文件的数据和。具体实现是在主线程中创建了一个CyclicBarrier
对象,并设置了一个屏障动作,当所有线程都到达屏障后,会执行指定的屏障动作,即计算所有 sheet 页的数据和。 -
多线程并发读取多个 Excel 文件的 sheet 页。使用线程池来并发读取多个 Excel 文件,并且对每个文件的每个 sheet 页进行并发读取。具体实现是在主方法中循环遍历多个 Excel 文件,对每个文件依次创建一个
Workbook
对象,并获取该文件内 sheet 页的数量,然后循环遍历每个 sheet 页,并使用线程池来并发读取每个 sheet 页的数据,并调用CyclicBarrier
的await()
方法等待其他线程读取完数据。 -
计算每个 sheet 页的数据和。在每个线程中实现
readSheetAndComputeSum()
方法,其中通过传入的sheetIndex
参数来获取对应的 sheet 页,并调用readSheet()
方法来计算该 sheet 页的数据和。然后将该 sheet 页的数据和存储到一个SheetSumWrapper
类中(该类使用了线程安全的Map
类型来存储每个 Excel 文件内每个 sheet 页的数据和)。 -
输出结果。在
CyclicBarrier
的屏障动作中,通过遍历SheetSumWrapper
中保存的每个 Excel 文件内每个 sheet 页的数据和,来输出每个文件的 sheet 页数量和数据和,并且统计所有文件的数据和。最终输出所有 Excel 文件的数据和。
4.5、疑问
1、上述代码是如何保证getTotalSheetCount() 计算的数量,和当前文件保持一一对应呢?
getTotalSheetCount() 方法确保计算的工作表数量与当前文件的工作表数量一一对应。这是通过在计算总数时使用 try-with-resources 块
打开每个文件并遍历工作表来实现的。
具体而言,getTotalSheetCount() 方法首先定义了变量 totalSheetCount,用于记录所有文件的工作表总数。然后,它遍历文件路径数组
FILE_PATHS,对于每个文件,使用 try-with-resources 打开一个 Workbook 对象并加载该文件。然后,通过
workbook.getNumberOfSheets() 方法获取该文件的工作表数量,并将其添加到 totalSheetCount 变量中。最后,返回 totalSheetCount
作为结果。
因此,getTotalSheetCount() 方法会计算所有文件的工作表总数,并确保与当前文件的工作表数量一一对应。这样,在创建线程并调用
readSheetAndComputeSum() 方法时,可以正确地设置 CyclicBarrier 的计数器值,以便等待所有线程完成。
2、那如何保证getTotalSheetCount方法和 主线程中的 for (String filePath : FILE_PATHS) 遍历的是一个文件呢?
确保 getTotalSheetCount() 方法返回的工作表总数与当前文件的工作表数量一一对应的关键是:
1、使用 try-with-resources 打开每个文件并加载该文件
2、对于每个文件,使用 workbook.getNumberOfSheets() 方法获取该文件的工作表数量,并将其添加到 totalSheetCount 变量中
由于这两个步骤都是按照数组 FILE_PATHS 中的顺序执行的,因此可以确保计算的工作表数量与遍历文件时打开的文件一一对应。因此,
不需要要求 FILE_PATHS 数组中文件的顺序性,只需要确保 FILE_PATHS 数组中的文件路径与实际要处理的文件路径一致即可。
5、Phaser
Phaser则是Java SE 7中引入的一个新的并发工具类,它提供了更为灵活和高级的线程同步机制。
Phaser可以用于同步多个线程的执行,将任务划分为多个阶段,并允许线程在每个阶段进行等待和继续执行。Phaser内部维护了一个计数器,用于追踪到达当前阶段的线程数量。当所有线程都到达当前阶段时,Phaser会进入下一个阶段,并唤醒所有等待的线程。
5.1、使用场景
Phaser适用于以下场景:
- 多阶段任务的并行处理:当一个任务可以划分为多个独立的阶段,并且每个阶段可以并行执行时,可以使用Phaser进行同步。例如,某个计算任务可以分为数据准备、数据计算和结果合并等多个阶段,可以使用Phaser来协调线程在不同阶段的执行。
- 线程循环协作:当一组线程需要循环执行某个任务,并在每次循环结束后进行同步时,可以使用Phaser。Phaser提供了方便的循环同步功能,可以重复使用,适合处理需要反复执行的任务。
- 动态线程控制:当需要动态地增加或减少参与任务的线程数量时,Phaser可以提供灵活的线程控制。通过注册和注销线程,可以动态地调整并行任务的线程数。
5.2、内部源码方法说明
5.2.1、 构造方法
-
Phaser()
:创建一个Phaser对象,默认初始阶段数为0。 -
Phaser(int parties)
:创建一个Phaser对象,指定初始阶段数。
5.2.2、 注册与注销线程
-
int register()
: 注册一个新线程,返回当前线程在Phaser中的编号(唯一标识)。 -
int bulkRegister(int parties)
: 注册多个新线程,返回当前线程在Phaser中的编号。 -
void arriveAndDeregister()
: 线程到达当前阶段,并注销自己,不再参与后续阶段的同步;并让其他线程需要等待的个数减一。
5.2.3、 阶段同步方法
-
int arrive()
: 线程到达当前阶段,等待其他线程到达。返回当前阶段的编号。 -
int arriveAndAwaitAdvance()
: 线程到达当前阶段,并等待其他线程到达。如果当前线程是该阶段最后一个未到达的,则此方法直接返回下一个阶段的编号(阶段序号从0开始),同时其他线程的该方法也返回下一阶段的序号。 -
int awaitAdvance(int phase)
: 线程等待指定阶段的到达。
5.2.4、 动态调整阶段数
-
void forceTermination()
: 强制终止Phaser,唤醒所有等待的线程。 -
int getPhase()
: 获取当前阶段的编号。 -
int getRegisteredParties()
: 获取已注册的线程数量。 -
int getArrivedParties()
: 获取已到达当前阶段的线程数量。
5.3、执行原理
Phaser内部使用了AQS(AbstractQueuedSynchronizer)作为其同步机制的基础。它将线程的等待和唤醒操作委托给AQS来管理,并通过计数器来追踪线程的到达情况。
当一个线程调用arriveAndAwaitAdvance()
方法时,它会将计数器加一,并检查是否所有线程都已到达。如果是,则进入下一个阶段,并唤醒所有等待的线程。否则,当前线程会通过AQS的同步机制进入等待状态。
当一个线程调用arriveAndDeregister()
方法时,它会将计数器加一并注销自己,不再参与后续阶段的同步。Phaser会相应地调整计数器和阶段数。
5.4、代码示例
@Slf4j
public class PhaserExample {
private static ExecutorService executor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), Runtime.getRuntime().availableProcessors() << 2, 1200L,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(20000), new ThreadFactoryBuilder().setNameFormat("- 高高手动创建的线程池-%d").build(), new ThreadPoolExecutor.AbortPolicy());
public static void main(String[] args) {
int threadNum = 3;
Phaser phaser = new Phaser(threadNum);
for (int i = 0; i < threadNum; i++) {
int thread = i;
executor.execute(() -> {
log.info("Thread {}: Phase 1", thread);
phaser.arriveAndAwaitAdvance();
log.info("");
log.info("Thread {}: Phase 2", thread);
phaser.arriveAndAwaitAdvance();
log.info("");
log.info("Thread {}: Phase 3", thread);
phaser.arriveAndDeregister();
});
}
}
}
输出结果:
14:38:03.345 [- 高高手动创建的线程池-0] INFO com.public_wechat.PhaserExample - Thread 0: Phase 1
14:38:03.345 [- 高高手动创建的线程池-1] INFO com.public_wechat.PhaserExample - Thread 1: Phase 1
14:38:03.345 [- 高高手动创建的线程池-2] INFO com.public_wechat.PhaserExample - Thread 2: Phase 1
14:38:03.351 [- 高高手动创建的线程池-1] INFO com.public_wechat.PhaserExample -
14:38:03.351 [- 高高手动创建的线程池-2] INFO com.public_wechat.PhaserExample -
14:38:03.351 [- 高高手动创建的线程池-0] INFO com.public_wechat.PhaserExample -
14:38:03.351 [- 高高手动创建的线程池-1] INFO com.public_wechat.PhaserExample - Thread 1: Phase 2
14:38:03.351 [- 高高手动创建的线程池-2] INFO com.public_wechat.PhaserExample - Thread 2: Phase 2
14:38:03.351 [- 高高手动创建的线程池-0] INFO com.public_wechat.PhaserExample - Thread 0: Phase 2
14:38:03.351 [- 高高手动创建的线程池-2] INFO com.public_wechat.PhaserExample -
14:38:03.351 [- 高高手动创建的线程池-1] INFO com.public_wechat.PhaserExample -
14:38:03.351 [- 高高手动创建的线程池-0] INFO com.public_wechat.PhaserExample -
14:38:03.351 [- 高高手动创建的线程池-2] INFO com.public_wechat.PhaserExample - Thread 2: Phase 3
14:38:03.351 [- 高高手动创建的线程池-0] INFO com.public_wechat.PhaserExample - Thread 0: Phase 3
14:38:03.351 [- 高高手动创建的线程池-1] INFO com.public_wechat.PhaserExample - Thread 1: Phase 3
三、各工具类的比较
1、CountDownLatch与CyclicBarrier的比较
-
功能不同:
CountDownLatch
用于等待其他线程完成操作后再执行,它通过一个计数器来实现,当计数器减为 0 时,等待的线程会被唤醒继续执行。CyclicBarrier
用于控制多个线程之间的同步,它通过一个栅栏来实现,所有线程必须同时到达栅栏位置后才能继续执行。CountDownLatch
方法比较少,操作比较简单,而CyclicBarrier
提供的方法更多,比如能够通过getNumberWaiting()
,isBroken()
这些方法获取当前多个线程的状态,并且CyclicBarrier
的构造方法可以传入barrierAction
,指定当所有线程都到达时执行的业务功能;
-
重复使用:
CountDownLatch
是一次性的,一旦计数器减为0
,就不能再次使用。CyclicBarrier
可以被重复使用,当所有线程都到达栅栏位置后,栅栏会打开并允许线程继续执行,然后可以重置栅栏以便下一轮使用。
-
计数器控制:
CountDownLatch
的计数器不能被重置,一旦计数器归零,等待的线程就会被唤醒。CyclicBarrier
的计数器在每次所有线程都到达栅栏位置后会自动重置,可以被多次使用。
-
应用场景:
CountDownLatch
适合用于一组线程需要等待另一组线程完成后才能开始执行的场景,例如主线程等待子线程全部完成后再继续执行。CyclicBarrier
适合用于一组线程需要相互等待,然后在同一时间点进行下一步操作的场景,例如多个线程协同完成某个复杂任务后再进行下一步处理。
2、CountDownLatch和Semaphore比较
-
功能区别:
- CountDownLatch: 用于等待其他线程完成操作后再执行。它通过一个计数器实现,计数器初始值设定为线程数量,每个线程完成任务时将计数器减1,当计数器归零时,等待的线程将被唤醒继续执行。
- Semaphore: 用于控制同时访问某个资源的线程数量。它维护着一定数量的许可证,线程在访问资源之前必须先获得许可证,如果许可证数量为0,则线程将被阻塞,直到有其他线程释放许可证。
-
计数方式区别:
- CountDownLatch: 使用一个计数器来控制等待的线程数量。计数器的初始值由用户设置,每个线程完成任务时调用
countDown()
方法来减少计数器的值,而等待的线程调用await()
方法来等待计数器归零。 - Semaphore: 使用一个指定数量的许可证来控制同时访问的线程数量。线程在访问资源之前,需要调用
acquire()
方法来获取许可证,如果许可证数量为0,则线程将被阻塞,直到其他线程调用release()
方法释放许可证。
- CountDownLatch: 使用一个计数器来控制等待的线程数量。计数器的初始值由用户设置,每个线程完成任务时调用
-
可重用性区别:
- CountDownLatch: 是一次性的,计数器归零后不能再次使用。
- Semaphore: 可以被重复使用,通过调用
acquire()
和release()
方法动态地获取和释放许可证。
-
使用场景区别:
- CountDownLatch: 适用于等待多个线程完成某个任务后再执行下一步操作。例如,主线程等待多个工作线程完成任务后才继续执行。
- Semaphore: 适用于控制同时访问某个资源的线程数量。例如,限制数据库连接池的最大并发连接数。
3、CyclicBarrier和Phaser比较
-
功能区别:
- CyclicBarrier: 用于控制多个线程之间的同步,要求所有线程都到达栅栏位置后才能继续执行。它通过一个计数器实现,计数器初始值设定为线程数量,每个线程到达栅栏时将计数器减1,当计数器归零时,等待的线程将被唤醒继续执行。
- Phaser: 是一个更高级的栅栏,用于控制多个阶段的线程之间的同步。它可以分为多个阶段,每个阶段包含多个参与者(线程),在每个阶段结束时,只有所有参与者都完成了任务,才能进入下一个阶段。
-
可重用性区别:
- CyclicBarrier: 可以被重复使用。当所有线程到达栅栏位置后,栅栏会自动重置,可以继续使用。
- Phaser: 可以被重复使用。当所有参与者完成任务后,Phaser会自动进入下一阶段,可以继续使用。
-
控制方式区别:
- CyclicBarrier: 通过栅栏控制等待。当所有线程都到达栅栏位置后,栅栏会自动释放所有线程继续执行。
- Phaser: 通过阶段和参与者控制同步。每个阶段包含多个参与者(线程),在每个阶段结束时,只有所有参与者都完成了任务,才能进入下一个阶段。
-
动态性区别:
- CyclicBarrier: 栅栏的计数器是固定的,在创建时就已经确定了。
- Phaser: 可以动态地增加或减少参与者数量,而且可以在运行时动态地增加或删除阶段。
-
高级特性区别:
- CyclicBarrier: 没有其他高级特性。
- Phaser: 支持等待某个特定的参与者完成任务,还支持在某个特定的阶段结束时执行一些操作。
四、总结
具体选择使用哪个工具类时,需要根据具体的需求和场景来决定,合并使用多线程可以有效提高执行效率。