Java并发编程 之CountDownLatch、CyclicBarrier、Semaphore同步辅助类

本文介绍了CountDownLatch的计数倒计时机制,如何用于等待子线程完成;CyclicBarrier的循环栅栏特性及其在同步多线程场景的应用;以及Semaphore的信号量控制并发的示例。通过实例演示了这些工具在控制线程执行顺序和并发限制中的关键作用。

CountDownLatch(发射倒计时)

教室晚自习放学关灯,需要等所有学生上完自习离开教室后才能关灯;主线程:关灯,子线程:学生上完自习离开教室。关灯之后不能重新打开后再关闭了,是一次性的。

主线程阻塞直到一组子线程全部执行完后才被唤醒。
CountDownLatch可以理解为一个计数器,直到计数器为0了,等待的线程才执行。
CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,调用线程会被阻塞。其他线程调用countDown方法计数器减1(调用countDown方法时线程不会阻塞),当计数器的值变为0,因调用await方法被阻塞的线程会被唤醒,继续执行。

构造方法:

// count 就是需要等待的线程数量
public CountDownLatch(int count)

重要方法:

// 调用此方法的线程会被阻塞,直到 CountDownLatch 的 count 为 0
public void await() throws InterruptedException 

// 和上面的 await() 作用基本一致,只是可以设置一个最长等待时间
public boolean await(long timeout, TimeUnit unit) throws InterruptedException

// 会将 count 减 1
public void countDown() 

使用场景:
场景1:
主线程等待其他线程完成各自任务后再开始执行自己的任务。
代码实现:

public class CountDownLatchDemo {

    static class TaskThread extends Thread {
        CountDownLatch latch;
        public TaskThread(CountDownLatch latch) {
            this.latch = latch;
        }
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println(getName() + " Task is Done ");
                latch.countDown();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException{
        int threadNum = 10;
        CountDownLatch latch = new CountDownLatch(threadNum);
        for(int i = 0; i < threadNum; i++) {
            new TaskThread(latch).start();
        }
        System.out.println("Task Start!");
        // 等待CountDownLatch计数为0后执行
        latch.await();
        System.out.println("All Task is Done!");
    }
}

代码解释:

  • 设置 CountDownLatch 的等待线程数为 10
  • 开启 10 个线程,每个线程都会睡眠 1 秒,睡眠结束后就会调用 CountDownLatch 的 countDown() 方法
  • 主线程调用 CountDownLatch 的 await() 方法,所以会开始阻塞,直到 CountDownLatch 的 count 为 0 才继续执行

打印结果:

Task Start!
Thread-1 Task is Done
Thread-5 Task is Done
Thread-8 Task is Done
Thread-9 Task is Done
Thread-2 Task is Done
Thread-7 Task is Done
Thread-6 Task is Done
Thread-4 Task is Done
Thread-3 Task is Done
Thread-0 Task is Done
All Task is Done!

场景2:
同时启动多个线程。
如果想同时启动多个线程,实现最大的并行性,就可以使用 CountDownLatch。
代码实现一:

CountDownLatch countDownLatch = new CountDownLatch(1);
ExecutorService executorService = Executors.newFixedThreadPool(5);
List<Future<?>> futures = new ArrayList<>();
for(int i=0; i<5; i++){
    Future<?> future = executorService.submit(() -> {
        try {
            countDownLatch.await();
            Thread.sleep(3000L);
            System.out.println("Thread:" + Thread.currentThread().getName() + ",time: " + System.currentTimeMillis());
        } catch (Exception e) {
            e.printStackTrace();
        }
    });
    futures.add(future);
}
countDownLatch.countDown();
executorService.shutdown();
for (int i=0; i<futures.size(); i++){
    futures.get(i).get();
}
System.out.println("end...");

打印结果:

Thread:pool-1-thread-1,time: 1564034719458
Thread:pool-1-thread-2,time: 1564034719458
Thread:pool-1-thread-3,time: 1564034719458
Thread:pool-1-thread-5,time: 1564034719458
Thread:pool-1-thread-4,time: 1564034719458

Process finished with exit code 0

代码实现二:

public class CountDownLatchDemo {
    static class TaskThread extends Thread {
        CountDownLatch latch;
        public TaskThread(CountDownLatch latch) {
            this.latch = latch;
        }
        @Override
        public void run() {
            try {
                latch.await();
                System.out.println(getName() + " start " + System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    } 
    public static void main(String[] args) throws Exception{
        int threadNum = 10;
        CountDownLatch latch = new CountDownLatch(1);
        for(int i = 0; i < threadNum; i++) {
            TaskThread task = new TaskThread(latch);
            task.start();
        }
        Thread.sleep(1000);
        latch.countDown();
    }
}

代码解释:

  • 设置 CountDownLatch 等待线程数为 1
  • 开启 10 个线程,每个线程都会调用 CountDownLatch 的 await() 方法,这样每个线程都会被阻塞
  • 主线程休眠 1 秒后,调用 CountDownLatch 的 countDown() 方法,调用后就会唤醒所有等待的线程,所有等待的线程就会同时执行

打印结果:

Thread-2 start 1564035004459
Thread-4 start 1564035004459
Thread-0 start 1564035004459
Thread-7 start 1564035004459
Thread-5 start 1564035004459
Thread-3 start 1564035004459
Thread-1 start 1564035004459
Thread-6 start 1564035004459
Thread-9 start 1564035004459
Thread-8 start 1564035004459

Process finished with exit code 0

秦灭六国

import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {

    public static void main(String[] args) throws Exception {
        CountDownLatch countDownLatch = new CountDownLatch(5);

        for (int i = 1; i <= 5; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t" + "国,灭亡");
                countDownLatch.countDown();
            }, CountryEnum.forEach(i).getName()).start();
        }

        countDownLatch.await();
        System.out.println("秦统一");
    }
}

// 枚举可以作为简单版本的数据库
enum CountryEnum {
    ONE(1, "齐"), TWO(2, "楚"), THREE(3, "燕"),
    FOUR(4, "赵"), FIVE(5, "魏"), SIX(6, "韩");

    CountryEnum(Integer code, String name) {
        this.code = code;
        this.name = name;
    }

    private Integer code;
    private String name;

    public static CountryEnum forEach(int index) {
        CountryEnum[] countryEnums = CountryEnum.values();
        for (CountryEnum countryEnum : countryEnums) {
            if (index == countryEnum.getCode()) {
                return countryEnum;
            }
        }
        return null;
    }

    public Integer getCode() {
        return code;
    }

    public String getName() {
        return name;
    }
}

批处理

public static <T> void dispose(List<T> taskList, Consumer<? super T> consumer, Executor executor) throws InterruptedException {
    if (taskList == null || taskList.size() == 0) {
        return;
    }
    Objects.nonNull(consumer);
    CountDownLatch countDownLatch = new CountDownLatch(taskList.size());
    for (T item : taskList) {
        executor.execute(() -> {
            try {
                consumer.accept(item);
            } finally {
                countDownLatch.countDown();
            }
        });
    }
    countDownLatch.await();
}

ExecutorService executorService = Executors.newFixedThreadPool(10);
dispose(taskList, param -> {
    System.out.println(param);
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}, executorService);
executorService.shutdown();

CyclicBarrier(循环栅栏)

集齐七颗龙珠才能召唤神龙。

简介

中文意思是循环栅栏
作用:让所有线程都等待完成后才会继续下一步行动。
举个例子:就像生活中我们会约朋友们到某个餐厅一起吃饭,有些朋友可能会早到,有些朋友可能会晚到,但是这个餐厅规定必须等到所有人到齐之后才会让我们进去。这里的朋友们就是各个线程,餐厅就是 CyclicBarrier。

CyclicBarrier可以使一定数量的线程反复地在栅栏位置处汇集。当线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都到达栅栏位置。如果所有线程都到达栅栏位置,那么栅栏将打开,此时所有的线程都将被释放,而栅栏将被重置以便下次使用。

CyclicBarrier使用

构造方法:

public CyclicBarrier(int parties) {
    this(parties, null);
}
 
public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}
  • parties 参数表示屏障拦截的线程数量,每个线程使用await()方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
  • 第二个构造方法有一个 Runnable 参数,这个参数的意思是最后一个到达线程要做的任务。

await方法:

public int await() throws InterruptedException, BrokenBarrierException {
    try {
        // 不超时等待
        return dowait(false, 0L);
    } catch (TimeoutException toe) {
        throw new Error(toe); // cannot happen
    }
}

public int await(long timeout, TimeUnit unit)
    throws InterruptedException,
            BrokenBarrierException,
            TimeoutException {
    return dowait(true, unit.toNanos(timeout));
}
  • 线程调用 await() 表示自己已经到达栅栏,然后当前线程被阻塞,直到parties个参与线程调用了await方法。
  • BrokenBarrierException 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断或者超时

CyclicBarrier示例

public class CyclicBarrierDemo {
    // 集龙珠
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
            System.out.println("龙珠收集完成");
        });
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 1; i <= 7; i++) {
            int finalI = i;
            executorService.submit(() -> {
                try {
                    TimeUnit.SECONDS.sleep((long) new Random().nextInt(5) + 1);
                    System.out.println(Thread.currentThread().getName() + "\t收集到龙珠" + finalI);
                    cyclicBarrier.await();
                    TimeUnit.SECONDS.sleep((long) new Random().nextInt(5) + 1);
                    System.out.println(Thread.currentThread().getName() + "\t收集到龙珠" + finalI);
                    cyclicBarrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "Thread-" + i);

        }
        executorService.shutdown();
    }
}

在这里插入图片描述
所有的工作线程都运行await()方法之后都到达了栅栏位置,然后执行到达后的任务。
从打印结果可以看出,所有线程会等待全部线程到达栅栏之后才会继续执行,并且最后到达的线程会完成 Runnable 的任务。

CyclicBarrier使用场景

可以用于多线程计算数据,最后合并计算结果的场景。

Semaphore(信号量)

单机限流,控制线程的并发数。
抢车位。
简介:
Semaphore 主要用于控制当前活动线程的数目,每个线程都需要通过acquire()方法获取许可,使用release()释放许可。当活动线程数目等于最大许可后,那么在调用acquire()方法后,线程会进入等待队列,等待已获取许可的线程释放许可。

定义:

 // 参数permits表示许可数目,即同时可以允许多少线程进行访问
public Semaphore(int permits) {         
    sync = new NonfairSync(permits);
}
// 这个多了一个参数fair表示是否是公平的,即等待时间越久的越先获取许可
public Semaphore(int permits, boolean fair) {    
    sync = (fair)? new FairSync(permits) : new NonfairSync(permits);
}

几个重要方法:

// 获取一个许可
public void acquire() throws InterruptedException { }  
// 获取permits个许可   
public void acquire(int permits) throws InterruptedException { }  
// 释放一个许可 
public void release() { }   
// 释放permits个许可       
public void release(int permits) { }    

acquire()用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许可。
release()用来释放许可。注意,在释放许可之前,必须先获获得许可。
这4个方法都会被阻塞,如果想立即得到执行结果,可以使用下面几个方法:

// 尝试获取一个许可,若获取成功,则立即返回true,若获取失败,则立即返回false
public boolean tryAcquire() { }; 
// 尝试获取一个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false
public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { };  
// 尝试获取permits个许可,若获取成功,则立即返回true,若获取失败,则立即返回false
public boolean tryAcquire(int permits) { }; 
// 尝试获取permits个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException { }; 

限流使用案例:

public class SemaphoreDemo {

    // 请求总数
    public static int clientTotal = 10;
    // 同时并发执行的线程数
    public static int threadTotal = 5;
    // 已执行线程的数量
    public static int count = 0;

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        // 信号量,此处用于控制并发的线程数
        final Semaphore semaphore = new Semaphore(threadTotal);
        // 闭锁,可实现计数器递减
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            executorService.execute(() -> {
                try {
                    // 执行此方法用于获取执行许可,当总计未释放的许可数不超过5时,允许通行,否则线程阻塞等待,直到获取到许可。
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+":start");
                    add();
                    System.out.println(Thread.currentThread().getName()+":end");
                    // 释放许可
                    semaphore.release();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                // 闭锁减一
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();// 线程阻塞,直到闭锁值为0时,阻塞才释放,继续往下执行
        executorService.shutdown();
        System.out.println("执行的总数:" + count);
        return;
    }

    // 业务逻辑
    private static void add() throws InterruptedException {
        count++;
        Random random = new Random();
        int i = random.nextInt(3) + 1;
        Thread.sleep(i * 1000);
    }
}

打印结果: 发现总是只有5个线程在执行中,当其中一个线程执行完并释放许可后,就会唤醒一个等待的线程执行。

pool-1-thread-1:start
pool-1-thread-2:start
pool-1-thread-3:start
pool-1-thread-4:start
pool-1-thread-5:start
pool-1-thread-2:end
pool-1-thread-6:start
pool-1-thread-4:end
pool-1-thread-7:start
pool-1-thread-1:end
pool-1-thread-8:start
pool-1-thread-3:end
pool-1-thread-9:start
pool-1-thread-5:end
pool-1-thread-7:end
pool-1-thread-10:start
pool-1-thread-8:end
pool-1-thread-6:end
pool-1-thread-9:end
pool-1-thread-10:end
执行的总数:10

Process finished with exit code 0

抢车位案例

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class SemaphoreDemo {

    public static void main(String[] args) {
        // 模拟3个停车位
        Semaphore semaphore = new Semaphore(3);
        // 模拟6部汽车
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                try {
                    // 抢车位,如果没有车位会阻塞直到抢到车位才被唤醒
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "\t抢到车位");
                    int time = (int) (Math.random() * 10 + 1);
                    TimeUnit.SECONDS.sleep(time);
                    System.out.println(Thread.currentThread().getName() + "\t停" + time + "秒离开车位,空车一个车位");
                } catch (InterruptedException e) {

                } finally {
                    // 空出车位
                    semaphore.release();
                }
            }, "Thread-" + String.valueOf(i)).start();
        }
    }
}

执行结果:
在这里插入图片描述

三个同步辅助类总结

  • CountDownLatchCyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:
    CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;
    CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
    另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。

CountDownLatch是减法,减到0唤醒。
CyclicBarrier是加法,加到一定数量唤醒。

  • Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。比如:限流。

参考:
Java并发编程之CyclicBarrier详解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值