这三大并发工具类,你必须要会!

505 阅读7分钟

CyclicBarrier

在现实生活中会有这样的场景:在开始某项活动之前必须等待所有人员都到位才可以开始。例如,在篮球比赛中,必须等待5名球员上场后才能开始比赛。又如一个旅游团必须所有人到达同一个景点参观后,才能一起到下一个景点继续游玩。那么如果叫你使用多线程来实现这些场景,你会怎么实现呢?

  • 在Java.util.concurrent包中,为我们提供了一个并发工具类CyclicBarrier,CyclicBarrier也叫循环栅栏,是一个可循环利用的屏障。通过它可以实现让一组线程等待至某个状态之后全部同时执行。每个线程在到达栅栏的时候都会调用await()方法将自己阻塞,此时计数器会减1,当计数器减为0的时候所有因调用await方法而被阻塞的线程将被唤醒。叫做循环是因为当所有等待线程都被释放以后,CyclicBarrier还可以被重用(调用CyclicBarrier的reset()方法)。

CyclicBarrier使用方式(例子)

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
            System.out.println("集齐七张卡片,参与抽奖");
        });

        for (int i = 1; i <= 7; i++) {
            final int temp=i;
            new Thread(()->{
                try {
                    System.out.println(Thread.currentThread().getName()+"收集到第"+temp+"张");
                    //阻塞任务线程
                    cyclicBarrier.await();

                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }


    }
}

程序输出:
Thread-1收集到第2张
Thread-4收集到第5张
Thread-3收集到第4张
Thread-2收集到第3张
Thread-0收集到第1张
Thread-6收集到第7张
Thread-5收集到第6张
集齐七张卡片,参与抽奖

CyclicBarrier的常用方法

  • int await() 阻塞调用该方法的线程,等待其他线程到达屏障点

  • int await(long timeout, TimeUnit unit) 指定被阻塞线程的等待时间。

  • int getNumberWaiting()返回已到达屏障的线程数量。

  • int - getParties()返回CyclicBarrier屏障所需的线程数量,即需要多少个线程到达屏障点,所有被阻塞线程才能继续向下执行。

  • boolean isBroken()查询这个障碍是否处于损坏状态。

  • void reset()将屏障重置为初始状态。

原理

CyclicBarrier还是基于AQS实现的,内部维护parties记录总线程数,count用于计数,最开始count=parties,调用await()之后count原子递减,当count为0之后,再次将parties赋值给count,这就是复用的原理。

  1. 当子线程调用await()方法时,获取独占锁,同时对count递减,进入阻塞队列,然后释放锁
  2. 当第一个线程被阻塞同时释放锁之后,其他子线程竞争获取锁,操作同
  3. 直到最后count为0,执行CyclicBarrier构造函数中的任务,执行完毕之后子线程继续向下执行。

CountDownLatch

CountDownLatch也是在java.util.concurrent包下的另一个并发工具类,利用它可以实现类似倒计数器的功能,CountDownLatch可以等待多个线程执行完毕后再做一件事情。比如有一个任务 A,它要等待其他 4 个任务执行完毕之后才能执行,此时就可以利用 CountDownLatch来实现这种功能了。CountDownLatch提供了一个countDown()方法来操作计数器的值,每调用一次countDown()方法计数器就会减1,直到计数器的值减为0时,因调用CountDownLatch的await()方法而阻塞的线程都会被唤醒。

CountDownLatch使用方式(例子)

public static void main(String[] args) throws InterruptedException {
    //初始计数器数量
    CountDownLatch countDownLatch = new CountDownLatch(6);

    for (int i = 1; i <= 6; i++) {
       //在 Lambda表达式中是无法拿到for循环中的i变量的,可以声明一个final中间变量来获取
       final int temp=i;
       new Thread(()->{
            //计数器-1
            countDownLatch.countDown();
            System.out.println(Thread.currentThread().getName()+":"+" 执行完成 :"+temp);
        }).start();
    }
    //阻塞,直到子线程全部执行完毕,假设计数器变为0,countDownLatch.await()就会被唤醒,继续执行
    countDownLatch.await();

    System.out.println(Thread.currentThread().getName()+"全部任务执行完成");

}

程序输出:
Thread-0: 执行完成 :1
Thread-3: 执行完成 :4
Thread-2: 执行完成 :3
Thread-1: 执行完成 :2
Thread-4: 执行完成 :5
Thread-5: 执行完成 :6
main全部任务执行完成

🚨CountDownLatch也可以用于实现使多个线程同时执行,如指定初始化计数器值为1,在多个线程中(即子任务)中调用await()方法进行阻塞,当所有线程都调用了await()方法被阻塞后,主线程调用countDown()使计数器变为0,之前被阻塞的多个子任务就会被唤醒,这时多个子任务就会同时继续往下执行。

CyclicBarrier与CountDownLatch的区别

  • CountDownLatch的await()方法阻塞的是主线程或调用await()的线程,而CyclicBarrier的await()阻塞的是任务线程,主线程或调用线程不受影响。

  • CountDownLatch无法重置计数次数,而CyclicBarrier可以通过reset()方法来重用

  • CountDownLatch和CyclicBarrier都是用作多线程同步,都是基于AQS实现。

原理

CountDownLatch基于AQS实现,volatile变量state维持倒数状态,多线程共享变量可见。

  1. CountDownLatch通过构造函数初始化传入参数实际为AQS的state变量赋值,维持计数器倒数状态。
  2. 当主线程调用await()方法时,当前线程会被阻塞,当state不为0时进入AQS阻塞队列等待。
  3. 其他线程调用countDown()时,state值原子性递减,当state值为0的时候,唤醒所有调用await()方法阻塞的线程。

Semaphore

Semaphore也叫信号量,是一种用来控制同时访问共享资源的线程数量的并发工具。Semaphore通过协调各个线程,以保证各个线程合理地使用共享资源。比如可以把Semaphore比作我们小区的车库,车库的停车位数量是有限的,只允许同时停放3辆车,在这里3个停车位就是共享资源,如果车库中已停满3辆车,那么当有第4量车想要停进车库时,就必须等待,直到3辆车中有一辆驶离车库时,那么就允许后面一辆车停入车库。

Semaphore使用方式(例子)

public static void main(String[] args) { 
    //可用资源数目,可以用来限制线程数量,多个共享资源互斥使用,例如车位 
    Semaphore semaphore = new Semaphore(3); 
    for (int i = 1; i <= 6; i++) { 
        new Thread(()->{ 
            try { semaphore.acquire(); 
            System.out.println(Thread.currentThread().getName()+"抢到车位"); 
            //停2秒 ,即使睡眠也不会释放许可
            TimeUnit.MILLISECONDS.sleep(2); 
            // System.out.println(Thread.currentThread().getName()+"离开车位"); 
            } catch (InterruptedException e) { 
                e.printStackTrace(); 
            }finally { 
                semaphore.release(); 
            } 
        }, "线程"+String.valueOf(i) ).start(); 
     } 
} 
        
    输出结果: 
    线程1抢到车位 
    线程3抢到车位 
    线程2抢到车位 
    线程2离开车位 
    线程1离开车位 
    线程4抢到车位 
    线程3离开车位 
    线程6抢到车位 
    线程5抢到车位 
    线程5离开车位 
    线程6离开车位 
    线程4离开车位

Semaphore常用方法

  • void acquire() 许可数-1,从该信号量获取许可证,阻塞直到可用或线程被中断。

  • void acquire(int permits) 许可数 - permits,从该信号量获取给定数量的许可证,阻塞直到可用或线程被中断。

  • int availablePermits() 返回此信号量中当前可用的许可数。

  • void release() 许可数+1,释放许可证,将其返回到信号量。

  • void release(int permits) 许可数+permits,释放给定数量的许可证,将其返回到信号量。

  • boolean hasQueuedThreads() 查询是否有线程正在等待获取许可。

  • int getQueueLength() 返回等待获取许可的线程数的估计

总结

CyclicBarrier可以实现让一组线程等待至某个状态之后全部同时执行,在这个过程中阻塞的是多个任务线程;CountDownLatch可以实现一个线程等待其他多个线程执行完毕后再继续执行,阻塞的是主线程或者调用线程;Semaphore信号量可以实现控制同时访问共享资源的线程数量,保证各个线程合理地使用共享资源。

以上就是对CyclicBarrier、CountLatch、Semaphore三个并发工具的简单描述,如果有错误的地方,还请留言指正,如果觉得本文对你有帮助那就点个赞吧︿( ̄︶ ̄)︿

默认标题_动态分割线_2021-07-15-0.gif