并发工具类大集合 - java.util.concurrent包

一、并发工具类介绍

通常我们所说的并发工具包,即为 java.util.concurrent 包,内部包含了 java 并发的各种工具类,若合理的使用能够帮助我们快速完成一些功能。

以下将介绍主要的工具类,以及内部主要的使用方法和原理,并提供代码示例

工具类名作用说明作用简述
CountDownLatchjdk1.5引入;是一个同步计数器,初始化的时候 传入需要计数的线程等待数,可以是需要等待执行完成的线程数等待其他线程完成,并当计数器减为0后在向下执行
Semaphore简称 “信号量”,在jdk1.5引入;可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。用于控制同时访问某个资源的线程数量,通常用于”限流“
Exchanger是一个线程间交换数据的工具类。它允许两个线程在彼此之间交换对象,并提供了一个方法:exchange()。调用 exchange() 方法会阻塞当前线程,直到另一个线程调用了相同的方法。两个线程交换数据
CyclicBarrierCyclicBarrier 是一个栅栏,用于控制多个线程之间的同步。它通过一个指定的计数器来实现,并提供了await()reset()两个方法。await() 方法用于阻塞当前线程,直到所有线程都到达了栅栏位置,reset() 方法用于重置计数器。作用跟CountDownLatch类似,但是可以重复使用
PhaserPhaser 是一个更加高级的栅栏,它可以用于控制多个阶段的线程之间的同步。它通过一个指定的阶段数和参与者数量来实现,并提供了多个方法: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号桌 就餐就餐完毕,正在退场...,就餐时长为:4011: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号桌 就餐就餐完毕,正在退场...,就餐时长为:5011:35:07.285 [Thread-1] INFO com.public_wechat.SemaphoreTests - 客人:1,在 2号桌 就餐就餐完毕,正在退场...,就餐时长为:5011:35:07.285 [Thread-0] INFO com.public_wechat.SemaphoreTests - 客人:0,在 1号桌 就餐就餐完毕,正在退场...,就餐时长为:5011:35:07.285 [Thread-5] INFO com.public_wechat.SemaphoreTests - 客人:5,在 6号桌 就餐就餐完毕,正在退场...,就餐时长为:5011: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号桌 就餐就餐完毕,正在退场...,就餐时长为:5011: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号桌 就餐就餐完毕,正在退场...,就餐时长为:3011:35:37.298 [Thread-8] INFO com.public_wechat.SemaphoreTests - 客人:8,在 1号桌 就餐就餐完毕,正在退场...,就餐时长为:3011:35:47.293 [Thread-11] INFO com.public_wechat.SemaphoreTests - 客人:11,在 2号桌 就餐就餐完毕,正在退场...,就餐时长为:4011:35:47.293 [Thread-9] INFO com.public_wechat.SemaphoreTests - 客人:9,在 5号桌 就餐就餐完毕,正在退场...,就餐时长为:4011:35:47.293 [Thread-10] INFO com.public_wechat.SemaphoreTests - 客人:10,在 6号桌 就餐就餐完毕,正在退场...,就餐时长为:4011:35:47.308 [Thread-6] INFO com.public_wechat.SemaphoreTests - 客人:6,在 4号桌 就餐就餐完毕,正在退场...,就餐时长为:50

Semaphore内部有一个继承了AQS的同步器Sync,重写了tryAcquireShared方法。在这个方法内会去尝试获取资源。如果获取失败,就会返回一个负数(代表尝试获取资源失败)。然后当前线程就会进入AQS的等待队列

3、Exchanger

3.1、使用场景

Exchanger 的主要使用场景是在两个线程之间安全地交换数据。它可以用于以下情况:

  1. 线程间的数据传递:两个线程之间需要传递数据,并且需要确保数据的完整性和一致性。

  2. 生产者-消费者模式:一个线程用于生成数据,另一个线程用于消费数据,Exchanger 用于在它们之间进行数据交换。

3.2、内部源码方法说明

exchange(V x):该方法用于将数据 x 交换给另一个线程,并返回另一个线程传递过来的数据。如果当前线程先调用 exchange() 方法,它将会阻塞等待另一个线程也调用 exchange() 方法,然后交换数据。

exchange(V x, long timeout, TimeUnit unit):该方法与上述方法类似,但是在超时时间内如果没有另一个线程调用 exchange() 方法,则当前线程将继续执行,并返回一个默认值。

3.3、执行原理

Exchanger 的执行原理如下:

  1. 当一个线程调用 exchange() 方法时,它会进入等待状态,直到另一个线程也调用 exchange() 方法。
  2. 当第二个线程调用 exchange() 方法时,两个线程会交换数据,并返回对方传递过来的数据。
  3. 如果有多个线程同时调用 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数据和完毕!!!

对上述代码示例的解释说明:

  1. 使用 CyclicBarrier 来实现多线程同时读取完所有 sheet 页后,再计算每个 sheet 页的数据和,最终输出所有 Excel 文件的数据和。具体实现是在主线程中创建了一个 CyclicBarrier 对象,并设置了一个屏障动作,当所有线程都到达屏障后,会执行指定的屏障动作,即计算所有 sheet 页的数据和。

  2. 多线程并发读取多个 Excel 文件的 sheet 页。使用线程池来并发读取多个 Excel 文件,并且对每个文件的每个 sheet 页进行并发读取。具体实现是在主方法中循环遍历多个 Excel 文件,对每个文件依次创建一个 Workbook 对象,并获取该文件内 sheet 页的数量,然后循环遍历每个 sheet 页,并使用线程池来并发读取每个 sheet 页的数据,并调用 CyclicBarrierawait() 方法等待其他线程读取完数据。

  3. 计算每个 sheet 页的数据和。在每个线程中实现 readSheetAndComputeSum() 方法,其中通过传入的 sheetIndex 参数来获取对应的 sheet 页,并调用 readSheet() 方法来计算该 sheet 页的数据和。然后将该 sheet 页的数据和存储到一个 SheetSumWrapper 类中(该类使用了线程安全的 Map 类型来存储每个 Excel 文件内每个 sheet 页的数据和)。

  4. 输出结果。在 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适用于以下场景:

  1. 多阶段任务的并行处理:当一个任务可以划分为多个独立的阶段,并且每个阶段可以并行执行时,可以使用Phaser进行同步。例如,某个计算任务可以分为数据准备、数据计算和结果合并等多个阶段,可以使用Phaser来协调线程在不同阶段的执行。
  2. 线程循环协作:当一组线程需要循环执行某个任务,并在每次循环结束后进行同步时,可以使用Phaser。Phaser提供了方便的循环同步功能,可以重复使用,适合处理需要反复执行的任务。
  3. 动态线程控制:当需要动态地增加或减少参与任务的线程数量时,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的比较

  1. 功能不同:

    • CountDownLatch 用于等待其他线程完成操作后再执行,它通过一个计数器来实现,当计数器减为 0 时,等待的线程会被唤醒继续执行。
    • CyclicBarrier 用于控制多个线程之间的同步,它通过一个栅栏来实现,所有线程必须同时到达栅栏位置后才能继续执行。
    • CountDownLatch方法比较少,操作比较简单,而CyclicBarrier提供的方法更多,比如能够通过getNumberWaiting()isBroken()这些方法获取当前多个线程的状态,并且CyclicBarrier的构造方法可以传入barrierAction,指定当所有线程都到达时执行的业务功能;
  2. 重复使用:

    • CountDownLatch 是一次性的,一旦计数器减为 0,就不能再次使用。
    • CyclicBarrier 可以被重复使用,当所有线程都到达栅栏位置后,栅栏会打开并允许线程继续执行,然后可以重置栅栏以便下一轮使用。
  3. 计数器控制:

    • CountDownLatch 的计数器不能被重置,一旦计数器归零,等待的线程就会被唤醒。
    • CyclicBarrier 的计数器在每次所有线程都到达栅栏位置后会自动重置,可以被多次使用。
  4. 应用场景:

    • CountDownLatch 适合用于一组线程需要等待另一组线程完成后才能开始执行的场景,例如主线程等待子线程全部完成后再继续执行。
    • CyclicBarrier 适合用于一组线程需要相互等待,然后在同一时间点进行下一步操作的场景,例如多个线程协同完成某个复杂任务后再进行下一步处理。

2、CountDownLatch和Semaphore比较

  1. 功能区别:

    • CountDownLatch: 用于等待其他线程完成操作后再执行。它通过一个计数器实现,计数器初始值设定为线程数量,每个线程完成任务时将计数器减1,当计数器归零时,等待的线程将被唤醒继续执行。
    • Semaphore: 用于控制同时访问某个资源的线程数量。它维护着一定数量的许可证,线程在访问资源之前必须先获得许可证,如果许可证数量为0,则线程将被阻塞,直到有其他线程释放许可证。
  2. 计数方式区别:

    • CountDownLatch: 使用一个计数器来控制等待的线程数量。计数器的初始值由用户设置,每个线程完成任务时调用countDown()方法来减少计数器的值,而等待的线程调用await()方法来等待计数器归零。
    • Semaphore: 使用一个指定数量的许可证来控制同时访问的线程数量。线程在访问资源之前,需要调用acquire()方法来获取许可证,如果许可证数量为0,则线程将被阻塞,直到其他线程调用release()方法释放许可证。
  3. 可重用性区别:

    • CountDownLatch: 是一次性的,计数器归零后不能再次使用。
    • Semaphore: 可以被重复使用,通过调用acquire()release()方法动态地获取和释放许可证。
  4. 使用场景区别:

    • CountDownLatch: 适用于等待多个线程完成某个任务后再执行下一步操作。例如,主线程等待多个工作线程完成任务后才继续执行。
    • Semaphore: 适用于控制同时访问某个资源的线程数量。例如,限制数据库连接池的最大并发连接数。

3、CyclicBarrier和Phaser比较

  1. 功能区别:

    • CyclicBarrier: 用于控制多个线程之间的同步,要求所有线程都到达栅栏位置后才能继续执行。它通过一个计数器实现,计数器初始值设定为线程数量,每个线程到达栅栏时将计数器减1,当计数器归零时,等待的线程将被唤醒继续执行。
    • Phaser: 是一个更高级的栅栏,用于控制多个阶段的线程之间的同步。它可以分为多个阶段,每个阶段包含多个参与者(线程),在每个阶段结束时,只有所有参与者都完成了任务,才能进入下一个阶段。
  2. 可重用性区别:

    • CyclicBarrier: 可以被重复使用。当所有线程到达栅栏位置后,栅栏会自动重置,可以继续使用。
    • Phaser: 可以被重复使用。当所有参与者完成任务后,Phaser会自动进入下一阶段,可以继续使用。
  3. 控制方式区别:

    • CyclicBarrier: 通过栅栏控制等待。当所有线程都到达栅栏位置后,栅栏会自动释放所有线程继续执行。
    • Phaser: 通过阶段和参与者控制同步。每个阶段包含多个参与者(线程),在每个阶段结束时,只有所有参与者都完成了任务,才能进入下一个阶段。
  4. 动态性区别:

    • CyclicBarrier: 栅栏的计数器是固定的,在创建时就已经确定了。
    • Phaser: 可以动态地增加或减少参与者数量,而且可以在运行时动态地增加或删除阶段。
  5. 高级特性区别:

    • CyclicBarrier: 没有其他高级特性。
    • Phaser: 支持等待某个特定的参与者完成任务,还支持在某个特定的阶段结束时执行一些操作。

四、总结

具体选择使用哪个工具类时,需要根据具体的需求和场景来决定,合并使用多线程可以有效提高执行效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值