Java--JUC之CountDownLatch、Semaphore以及CyclicBarrier

CountDownLatch

概念

  • 一种同步帮助,允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。

  • 一个CountDownLatch与给定数初始化。await方法块直到当前计数达到零的countDown()方法调用,之后,所有等待的线程,释放任何后续的调用await立即返回。这是一一个镜头的现象-计数不能被重置。如果你需要一个版本,重置计数,考虑使用CyclicBarrier

  • 一个CountDownLatch是一种通用的同步工具,可以用于许多用途。一个CountDownLatch初始化计算的一个作为一个简单的开/关锁,门:所有线程调用await在门口等候直到它被一个线程调用countDown()打开。一个CountDownLatch初始化n可以用来使一个线程等待直到n个线程完成一些动作,或一些行动已经完成N次。

  • 一个CountDownLatch是一个有用的特性,它不需要线程countDown等待计数达到零之前,它只是防止任何线程进行过await直到所有的线程可以通过。

-------------源自JDK1.8 API文档

那么其实CountDownLatch就是一个同步工具,让一个或多个线程进入等待,知道其他线程执行完毕后来唤醒这些等待的线程,采用的方式就是声明CountDownLatch给声明一个数量值,当这个数值变为0时,其他阻塞的线程被唤醒进入就绪状态,且这个工具的计数是不能重置的,如果需要的业务场景要用到重置的计数,可以选择使用CyclicBarrier同步工具。

方法

Modifier and Type Method and Description
void await() 使当前线程等待直到锁向下计数为零,除非线程 interrupted。
boolean await(long timeout, TimeUnit unit) 使当前线程等待直到锁向下计数为零,除非线程 interrupted,或指定的等待时间的流逝。
void countDown() 减少锁的数量,释放所有等待的线程,如果计数为零。
long getCount() 返回当前计数。
String toString() 返回一个识别该锁存器的字符串,以及它的状态。

有几个需要注意的点:

  • CountDownLatch(int count)构造函数中的count是计数器的初始值,也就是说countDown()方法至少被调用count次线程才会被唤醒,如果count<0那么会抛出异常

  • countDown()方法,调用一次count-1,如果count已经是0了,那么再调用此方法时什么都不做,此方法不会阻塞

    public static void main(String[] args) {
        CountDownLatch latch = new CountDownLatch(2);
        System.out.println(latch.getCount());
        latch.countDown();
        System.out.println(latch.getCount());
        latch.countDown();
        System.out.println(latch.getCount());
        latch.countDown();
        System.out.println(latch.getCount());
    }
    
    2
    1
    0
    0
    
  • await()方法中两种情况会抛出异常:1:调用await()前,线程被中断了 2:调用await()后线程在等待期间被中断了

    // 在调用`await()`方法前,当前线程的中断状态已经为 true 了
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch cdl = new CountDownLatch(1);
        Thread.currentThread().interrupt();
        cdl.await();
    }
    
    // 在等待的过程中被中断了
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch cdl = new CountDownLatch(1);
        Thread t1 = new Thread(() -> {
            try {
                cdl.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t1.start();
        Thread.sleep(500);
        t1.interrupt();
    }
    
  • await(long timeout, TimeUnit unit) 方法是有返回值的,为当前线程的阻塞设置一个等待时间,时间过后自动唤醒线程,若timeout值<=0那么不会进行线程阻塞;当计数器的值是0时,该方法返回值true,否则返回false

案例

一个简单的案例,主线程执行完毕后,子线程开始执行。

public class CountDownLatchTest {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(4);
        new Thread(new CDLAble(latch)).start();
        for (int i = 1; i < 5; i++) {
            System.out.println("主进程执行第" + i + "遍");
            latch.countDown();
        }
    }
}

@Data
class CDLAble implements Runnable {
    CountDownLatch latch;

    CDLAble(CountDownLatch latch) {
        this.latch = latch;
    }

    @SneakyThrows
    @Override
    public void run() {
        latch.await();
        System.out.println("我是子进程,开始执行了");
    }
}

假如涉及到主线程进行某时间后,子线程不管计数器是否已经为0都进行执行任务,那么可以使用await(long timeout, TimeUnit unit) 方法。

CyclicBarrier

概念

一个同步帮助,允许一组线程相互等待,以达到一个共同的障碍点。该障碍被称为循环,因为它可以在等待线程被释放后重新使用。

CyclicBarrier可以理解为是一种循环栅栏,如学校运动会的百米冲刺,需要10个人先就位到达起跑点(可理解为栅栏),然后枪响开始起跑;而CyclicBarrier就是一种循环栅栏,可以复用计数器,这点是与CountDownLatch不同的。

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

构造函数

  • 函数一
public CyclicBarrier(int parties)

CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程使用await()方法告诉CyclicBarrier当前线程已经到达了屏障,然后当前线程被阻塞。

其中线程必须要调用await()方法到达栅栏位置进行阻塞,并且如果parties小于1将会抛出异常。

  • 函数二
public CyclicBarrier(int parties, Runnable barrierAction)

CyclicBarrier的另一个构造函数CyclicBarrier(int parties, Runnable barrierAction),用于所有线程线程到达屏障时,最后到达的线程执行barrierAction,方便处理更复杂的业务场景。

贴一下源码,其实函数一就是调用的函数二

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;
}

方法

Modifier and Type Method and Description
int await() 等待其他线程到达 await这个屏障。
int await(long timeout, TimeUnit unit) 等到所有的 线程 await这个障碍,或指定的等待时间。
int getNumberWaiting() 返回当前正在等待的障碍物的数量。
int getParties() 返回访问此障碍所需的线程数。
boolean isBroken() 查询,这个障碍是否是一个破碎的状态。
void reset() 重置为其初始状态的屏障。计数器重置
  • 调用await方法的线程告诉CyclicBarrier自己已经到达同步点,然后当前线程被阻塞。直到parties中所有参与线程调用了await方法,CyclicBarrier提供带超时时间的await和不带超时时间的await方法

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

    CyclicBarrier的核心方法就是这个dowait(),底层用的ReentrantLock来实现

    private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
    TimeoutException {
        final ReentrantLock lock = this.lock;
        // 获取锁
        lock.lock();
        try {
            // 分代
            final Generation g = generation;
    
            // 当前generation“已损坏”,抛出BrokenBarrierException异常
            // 抛出该异常一般都是某个线程在等待某个处于“断开”状态的CyclicBarrie
            if (g.broken)
                // 当某个线程试图等待处于断开状态的 barrier 时,或者 barrier 进入断开状态而线程处于等待状态时,抛出该异常
                throw new BrokenBarrierException();
    
            // 如果线程中断,终止CyclicBarrier
            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
            }
            // 进来一个线程 count - 1
            int index = --count;
            // count == 0 表示所有线程均已到位,触发Runnable任务
            if (index == 0) {  // tripped
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand;
                    // 执行任务
                    if (command != null)
                        command.run();
                    ranAction = true;
                    //唤醒所有等待线程,并更新generation
                    nextGeneration();
                    return 0;
                } finally {
                    if (!ranAction)
                        breakBarrier();
                }
            }
    
            // loop until tripped, broken, interrupted, or timed out
            for (;;) {
                try {
                    // 如果不是超时等待,则调用Condition.await()方法等待
                    if (!timed)
                        trip.await();
                    else if (nanos > 0L)
                        // 超时等待,调用Condition.awaitNanos()方法等待
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                    if (g == generation && ! g.broken) {
                        breakBarrier();
                        throw ie;
                    } else {
                        // We're about to finish waiting even if we had not
                        // been interrupted, so this interrupt is deemed to
                        // "belong" to subsequent execution.
                        Thread.currentThread().interrupt();
                    }
                }
    
                if (g.broken)
                    throw new BrokenBarrierException();
                // generation已经更新,返回index
                if (g != generation)
                    return index;
                // “超时等待”,并且时间已到,终止CyclicBarrier,并抛出异常
                if (timed && nanos <= 0L) {
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            // 解锁
            lock.unlock();
        }
    }
    

    所有的线程调用await()进入阻塞状态,每有一个线程达到barrier屏障点时,计数器数量就-1,当数量到达0时,所有的线程将会被释放进入就绪状态,若barrier构造函数中有设置回调函数的任务,那么最后一个线程就会去执行这个回调函数来执行任务。

案例

假如军训时,教官让同学们分为两队集合进行报到,一队先进行报到,5s后二队进行报到,并且由每队最后就位的同学汇报队伍集合完毕

public class CyclicBarrierTest {
    public static void main(String[] args) throws InterruptedException {
        int count = 6;
        CyclicBarrier barrier = new CyclicBarrier(count, () -> System.out.println("报告,队伍集合完毕!"));

        for (int i = 0; i < count; i++) {
            // 开始进行报到
            new Thread(new CBAble((i + 1) + "号", barrier)).start();
        }
        // 重置计数
        barrier.reset();
        Thread.sleep(5000);

        for (int i = count; i < count*2; i++) {
            // 开始进行报到
            new Thread(new CBAble((i + 1) + "号", barrier)).start();
        }

    }
}

class CBAble implements Runnable {

    private String name;
    private CyclicBarrier barrier;

    CBAble(String name, CyclicBarrier barrier) {
        this.name = name;
        this.barrier = barrier;
    }

    @SneakyThrows
    @Override
    public void run() {
        System.out.println("报告,我是" + name + ",前来报到!");
        barrier.await();
    }
}
报告,我是1号,前来报到!
报告,我是6号,前来报到!
报告,我是5号,前来报到!
报告,我是4号,前来报到!
报告,我是3号,前来报到!
报告,我是2号,前来报到!
报告,队伍集合完毕!
报告,我是7号,前来报到!
报告,我是8号,前来报到!
报告,我是11号,前来报到!
报告,我是10号,前来报到!
报告,我是9号,前来报到!
报告,我是12号,前来报到!
报告,队伍集合完毕!

CountDownLatch&CyclicBarrier区别

  • CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置,可以使用多次,所以CyclicBarrier能够处理更为复杂的场景;
  • CyclicBarrier还提供了一些其他有用的方法,比如getNumberWaiting()方法可以获得CyclicBarrier阻塞的线程数量,isBroken()方法用来了解阻塞的线程是否被中断;
  • CountDownLatch允许一个或多个线程等待一组事件的产生,而CyclicBarrier用于等待其他线程运行到栅栏位置。

Semaphore

概念

Semaphore这个英文单词的意思是信号灯,即发送信号的那种灯,Java并发包中的Semaphore类是线程之间互相发送信号的工具。作用:限制某段代码块的并发数量,表示某段代码只有n个线程可以访问,如果超出n,其他线程需要进行等待,等某个线程执行完这段代码后,下一个线程拿到许可证可以进入此代码块中执行。由此能看出,如果这段代码块中只允许一个线程访问,那么就无异于使用悲观锁了。

线程可以通过acquire()方法获取到一个许可,然后对共享资源进行操作。注意如果许可已分配完了,那么线程将进入等待状态,直到其他线程释放许可才有机会再获取许可,线程释放一个许可通过release()方法完成,"许可"将被归还给Semaphore

Semaphore与ReentrantLock一样基于AQS的子类Sync并有公平和非公平模式。Sync继承自AQS,而NonfairSyncFairSync继承自Sync。
Java--JUC之CountDownLatch、Semaphore以及CyclicBarrier_第1张图片

构造函数

  • 函数一
public Semaphore(int permits)

创建一个 Semaphore与给定数量的许可证和nonfair公平设置,线程访问时采用非公平策略

  • 函数二
public Semaphore(int permits, boolean fair)

创建一个 Semaphore与给定数量的许可证,线程访问时的公平策略根据第二个参数的值确定。

方法

Modifier and Type Method and Description
void acquire() 从这个信号获取许可证,阻塞直到有一个可用的线程,或是 interrupted。
void acquire(int permits) 获得了从这个信号量许可证号码,阻塞直到所有的线程可用,或是 interrupted。
void acquireUninterruptibly() 从这个信号获取许可证,阻塞直到有可用的。
void acquireUninterruptibly(int permits) 获得了从这个信号量许可证号码,阻塞直到所有可用。
int availablePermits() 返回当前在该信号量可用许可证号码。
int drainPermits() 获取并返回立即可用的所有许可证。
protected Collection getQueuedThreads() 返回一个包含可能等待获取的线程的集合。
int getQueueLength() 返回等待获取的线程数的估计值。
boolean hasQueuedThreads() 查询是否有任何线程等待获取。
boolean isFair() 如果这个信号返回 true公平设置为true。
protected void reducePermits(int reduction) 减少了可用的许可证的数量减少了。
void release() 发布许可证,返回到信号。
void release(int permits) 释放一定数量的许可证,让他们回归的信号。
String toString() 返回一个字符串识别这种信号,以及其状态。
boolean tryAcquire() 从这个信号获取许可证,只有一个人可以在调用时。
boolean tryAcquire(int permits) 获得了从这个信号量许可证号码,只有所有都可以在调用时。
boolean tryAcquire(int permits, long timeout, TimeUnit unit) 获得了从这个信号量许可证号码,如果所有可用在给定的等待时间和当前线程没有被 interrupted。
boolean tryAcquire(long timeout, TimeUnit unit) 从这个信号获取许可证,如果有一个可用在给定的等待时间和当前线程没有被 interrupted。

比较常用的就是acquire()release()获取许可证和释放回许可证供其他线程进行访问代码块。其中有不少的方法都是直接调用的AQS类的具体实现(类内部的Sync继承自AQS类)

案例

10个线程访问一段代码块,规定同时只有5个线程能访问到。

public class SemaphoreTest {
    
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(5);
        for (int i = 0; i < 10; i++) {
            new Thread(new STAble((i + 1), semaphore)).start();
        }
    }
    
}

class STAble implements Runnable {

    private int name;
    private Semaphore semaphore;

    STAble(int name, Semaphore semaphore) {
        this.name = name;
        this.semaphore = semaphore;
    }

    @SneakyThrows
    @Override
    public void run() {
        semaphore.acquire();
        System.out.println(name + "获取资源");
        //处理业务
        Thread.sleep(1000);
        System.out.println(name + "释放资源");
        semaphore.release();
    }
}

每次只有5个线程在执行任务,其他线程只能等待,等5个线程中有线程释放许可证后,其他线程才能有获取许可证执行任务的机会。

1获取资源
3获取资源
2获取资源
5获取资源
4获取资源

2释放资源
3释放资源
1释放资源
4释放资源
5释放资源

9获取资源
8获取资源
7获取资源
6获取资源
10获取资源

8释放资源
10释放资源
7释放资源
9释放资源
6释放资源

参考

CountDownLatch 的用法

若要详细了解Semaphore请看此博客java.util.concurrent.Semaphore 类源码的深入解读

你可能感兴趣的:(Java,java,开发语言)