今天我们就详细介绍一下JUC的一些常用同步工具类,减少计数(CountDownLatch),循环栅栏(CyclicBarrier),信号灯(Semaphore)的使用和区别。

CountDownLatch

作用:就是一个或者多个线程在开始执行操作之前,必须要等到其他线程执行完成才可以执行。

类的构造方法
CountDownLatch(int count)构造一个用给定计数初始化的值。

public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

核心方法

countDown()递减锁存器的计数,如果计数达到零,将释放所有等待的线程

public void countDown() {
        sync.releaseShared(1);
}

await() 使当前线程在锁存器倒计数至零之前一直处于等待。

public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
}

当线程调用await()方法时,就会阻塞当前线程。当线程调用一次countDown()方法时,count就会减一,直到当count 的值等于0时候,被阻塞的线程才可以继续执行。

现在我们用一个生活中的例子说明:学生时代,当我们在考试的时候,监考老师必须等到所有的学生交完卷子才可以离开,此时监考老师就相当于等待线程,而学生就好比是执行的线程。

我们用代码实现这个案例:

参加考试的学生10个, main线程就相当于监考老师

public class CountDownLatchDemo {
    
   private static CountDownLatch countDownLatch = new CountDownLatch(10);
    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i <= 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"\t 学生交卷:");
                    // 减一
                    countDownLatch.countDown();
                }
            },String.valueOf(i)).start();
        }
        // 等待
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName()+"\t 监考老师离开教室");
    }
}

底层实现原理

从源码我们不难发现CountDownLatch是基于AQS实现的,当我们在构建CountDownLatch对象时,传入的值其实就会赋值给 AQS 的关键变量state,执行countDown()方法时,其实就是利用CAS 将state 减一,执行await()方法时,其实就是判断state是否为0,不为0则加入到队列中,将该线程阻塞掉(除了头节点),因为头节点会一直自旋等待state为0,当state为0时,头节点把剩余的在队列中阻塞的节点也一并唤醒。

CyclicBarrier

作用:N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。

常用的构造方法有:
CyclicBarrier(int parties,Runnable barrierAction)创建一个新的CyclicBarrier,它将在给定数量的线程处于等待状态时启动,并在启动barrier时执行给定的屏障操作,该操作由最后一个进入barrier的线程操作

 public CyclicBarrier(int parties, Runnable barrierAction) {
        if (parties <= 0) throw new IllegalArgumentException();
        this.parties = parties;
        this.count = parties;
        this.barrierCommand = barrierAction;
}

核心方法

await()在所有的参与者都已经在此barrier上调用await方法之前一直等待

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

生活中的例子:在打王者的时候,在开局前所有人都必须要加载到100%才可以进入。否则所有玩家都相互等待。

public class CyclicBarrierDemo {

    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier =new CyclicBarrier(5, () -> System.out.println("游戏开始"));

        for (int i = 1; i <=5 ; i++) {
            int finalI = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("第"+ finalI +"进入游戏");
                    try {
                        cyclicBarrier.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }
            },String.valueOf(i)).start();
        }
    }
}

总结:CyclicBarrier 的构造方法第一个参数是目标障碍数,每次执行 CyclicBarrier 一次障碍数会加一,如果达到了目标障碍数,才会执行 await()之后方法。

底层实现原理

从源码不难发现的是,它没有像CountDownLatchReentrantLock使用AQS的state变量,而CyclicBarrier是直接借助ReentrantLock加上Condition 等待唤醒的功能 进而实现的。

在构建CyclicBarrier时,传入的值会赋值给CyclicBarrier内部维护count变量,也会赋值给parties变量,每次调用await()方法时,会将count 减一 ,操作count值是直接使用ReentrantLock来保证线程安全性。如果count不为0,则添加Condition队列中,如果count等于0时,则把节点从Condition队列添加至AQS的队列中进行全部唤醒,并且将parties的值重新赋值为count的值。

Semaphore

信号量,用来控制同一时间,资源可被访问的线程数量,一般应用场景流量的控制。

构造方法
Semaphore(int permits)创建具有给定的许可数和非公平的公平设置的Semapore

public Semaphore(int permits) {
        sync = new NonfairSync(permits);
}

核心方法
acquire()从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断,release()释放一个许可,将其返回给信号量,设置许可数量Semaphore semaphore = new Semaphore(3),一般acquire()都会抛出异常,releasefinally中执行。

public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
}
public void release() {
        sync.releaseShared(1);
}

举例说明,6辆🚗强站三个停车位。

public class SemaphoreDemo {

    public static void main(String[] args) {
        // 模拟资源类,有3个空车位
        Semaphore semaphore = new Semaphore(3);
        for (int i = 1; i <=6 ; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"\t 抢到了车位");
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    semaphore.release();
                }
                System.out.println(Thread.currentThread().getName()+"\t 离开了车位");
                semaphore.release();
        },String.valueOf(i)).start();
        }
    }
}

Semaphore 底层实现是基于AQS实现的类似于CountDownLatch

总结

CountDownLatch,Semaphore都是基于AQS实现。

CountDownLatch是一个线程等待其他线程,CyclicBarrier是线程之间相互等待。

CountDownLatch会将构造CountDownLatch的入参传递至state,countDown()就是在利用CAS将state减一,await()实际就是让头节点一直在等待state为0时,释放所有等待的线程。

CyclicBarrier则利用ReentrantLockCondition,自身维护了count和parties变量。每次调用await将count减一,并将线程加入到Condition队列上。等到count为0时,则将Condition队列的节点移交至AQS队列,并全部释放。

文章链接:CountDownLatch,CyclicBarrier,Semaphore的使用及底层实现

👉 如果本文对你有帮助的话,欢迎点赞|关注,非常感谢


阿福研习社
57 声望43 粉丝