并发工具类

464 阅读3分钟

0. 前言

并发包里提供了几个非常有用的并发工具类,CountDownLatch、CyclicBarrier、Semaphore 工具类提供了一种并发流程控制的手段,Exchanger 工具类则提供了在线程间交换数据的手段

1. 等待多线程完成的 CountDownLatch

CountDownLatch 允许一个或多个线程等待其他线程完成操作

1.1 重要方法

  • CountDownLatch(int count) :构造方法 count 入参作为计数器
  • countDown() :计数器减 1
  • await() :线程一直被阻塞直到计数器为 0
  • await(long timeout, TimeUnit unit) :线程被阻塞一段时间后,超过指定时间后计数器还不为 0,则该线程不再阻塞而继续执行

1.2 案例

/**
 * 使用多线程解析一个 Excel 里多个 sheet 的数据
 * 主线程需要等待所有线程的解析完成后才继续执行
 */
public class Test {

    public static CountDownLatch c = new CountDownLatch(2);

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new ExcelRunner(3000));
        Thread thread2 = new Thread(new ExcelRunner(5000));

        thread1.start();
        thread2.start();

        c.await();

        System.out.println("main end");
    }

    static class ExcelRunner implements Runnable {

        private int parseTime;

        public ExcelRunner(int parseTime) {
            this.parseTime = parseTime;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(parseTime);
                System.out.println(Thread.currentThread().getName() + " 解析完毕");
                c.countDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

2. 同步屏障 CyclicBarrier

CyclicBarrier 同步屏障让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,所有被阻塞的线程不再阻塞而继续执行。可用于多线程计算数据,最后合并计算结果

2.1 构造方法

CyclicBarrier(int parties)
CyclicBarrier(int parties, Runnable barrierAction)
  • parties:屏障阻塞的线程数

  • barrierAction:最后一个到达屏障的线程,优先执行完 barrierAction 任务后,所有被阻塞的线程才可以继续执行

2.2 重要方法

  • await() :线程一直被屏障阻塞直到最后一个线程到达屏障
  • await(long timeout, TimeUnit unit) :线程被阻塞一段时间后,超过指定时间后最后一个线程还未到达屏障,则破坏屏障抛出异常

2.3 案例

/**
 * 一个 Excel 保存了多个用户的银行流水的 sheet
 * 需要统计每个 sheet 的的流水总和,再把所有 sheet 的流水总和加起来
 */
public class BankWaterService implements Runnable {

    private int sheetNum;

    private AtomicInteger sheetIndex = new AtomicInteger(1);

    private CyclicBarrier c;

    private ConcurrentHashMap<String, Integer> bankWaterMap = new ConcurrentHashMap<>();

    public BankWaterService(int sheetNum) {
        this.sheetNum = sheetNum;
        this.c = new CyclicBarrier(sheetNum, this);
    }

    public void count() {
        ExecutorService executor = Executors.newFixedThreadPool(sheetNum);
        for (int i = 0;i < sheetNum;i++) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(3000);
                        bankWaterMap.put("sheet" + sheetIndex.getAndIncrement(), 100);
                        c.await();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        executor.shutdown();

    }

    @Override
    public void run() {
        int total = 0;
        for (Map.Entry<String, Integer> entry : bankWaterMap.entrySet()) {
            total += entry.getValue();
        }
        bankWaterMap.put("total", total);
        System.out.println(bankWaterMap);
    }
}

public class Test {

    public static void main(String[] args) {
        BankWaterService service = new BankWaterService(10);
        service.count();
    }
}

2.4 CountDownLatch 和 CyclicBarrier 的区别

  • CountDownLatch 的计数器只能使用一次,CyclicBarrier 的计数器可以使用 reset() 方法重置

  • CyclicBarrier 提供了更多有用的方法

    • getNumberWaiting() 方法:获取屏障阻塞的线程数量

    • isBroken() 方法:判断阻塞的线程是否被中断

3. 控制并发线程数的 Semaphore

Semaphore 用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用公共资源。可用于做流量控制,特别是公共资源有限的应用场景,比如数据库连接

3.1 构造方法

Semaphore(int permits)
Semaphore(int permits, boolean fair)

permits:允许同时并发执行的线程数量

fair:公平性,如果设为 true 时,则下次执行的是等待最久的线程

3.2 重要方法

  • acquire() :阻塞线程获取执行许可
  • release() :线程释放执行许可

3.3 案例

/**
 * 有多个文件需要读取后保存到数据库
 * 因为都是 IO 密集型任务,所以开启多个线程并发读取后保存
 * 由于数据库连接数量有限,所以需要控制同时保存数据的线程数量
 */
public class Test {

    private static ExecutorService executor = Executors.newFixedThreadPool(30);

    private static Semaphore s = new Semaphore(10);

    public static void main(String[] args) {
        for (int i = 0;i < 30;i++) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(5000);
                        System.out.println(Thread.currentThread().getName() + " parse data end");
                        s.acquire();
                        Thread.sleep(2000);
                        System.out.println(Thread.currentThread().getName() + " save data end");
                        s.release();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        executor.shutdown();
    }
}

4. 线程间交换数据的 Exchanger

Exchanger 是一个用于线程间协作的工具类,可用于线程间交换数据。假设有两个线程交换数据,Exchanger 提供一个同步点,当两个线程都到达同步点时,这个两个线程就可以将自身生产的数据传递给对方

4.1 重要方法

  • exchange(V x) :线程等待另一个线程到达同步点时交换数据

  • exchange(V x, long timeout, TimeUnit unit) :线程等待另一个线程到达同步点时交换数据,如果超过指定时间后另一个线程还未到达同步点,则放弃等待抛出异常

4.2 案例

/**
 * 当银行将纸质的银行流水录入系统时,为了避免录入错误数据
 * 需要 A、B 两名员工录入数据后,进行数据校对
 */
public class Test {

    private static Exchanger<String> exchanger = new Exchanger();

    public static void main(String[] args) {
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(4000);
                    String aData = "流水数据";
                    exchanger.exchange(aData);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        threadA.start();

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    String bData = "流水数据";
                    String aData = exchanger.exchange(bData);
                    System.out.println("b员工校对结果 " + bData.equals(aData));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        threadB.start();
    }
}

学自《Java并发编程的艺术》