线程控制之CountDownLatch、CyclicBarrier、Semaphore

主线程与子线程

主线程

当一个程序启动时,就有一个进程被操作系统(OS)创建,与此同时一个线程也立刻运行,该线程通常叫做程序的主线程(Main Thread),因为它是程序开始时就执行的,如果你需要再创建线程,那么创建的线程就是这个主线程的子线程。每个进程至少都有一个主线程。

主线程的重要性体现在两方面:

(1)是产生其他子线程的线程;

(2)通常它必须最后完成执行比如执行各种关闭动作。

当 Java 程序启动时,一个线程会立刻运行,该线程通常叫做程序的主线程(main thread), 即 main 方法对应的线程,它是程序开始时就执行的。 Java 应用程序会有一个 main 方法,是作为某个类的方法出现的。当程序启动时,该方 法就会第一个自动的得到执行,并成为程序的主线程。也就是说,main 方法是一个应用的 入口,也代表了这个应用的主线程。JVM 在执行 main 方法时,main 方法会进入到栈内存,JVM 会通过操作系统开辟一条 main 方法通向 cpu 的执行路径,cpu 就可以通过这个路径来执行 main 方法,而这个路径有一个名字,叫 main(主)线程

主线程的特点:

它是产生其他子线程的线程。

它不一定是最后完成执行的线程,子线程可能在它结束之后还在运行。

子线程

在主线程中创建并启动的线程,一般称之为子线程。

CountDownLatch

CountDownLatch是一种通用的同步工具,可用于许多目的。

用1的计数初始化的CountDownLatch用作简单的开/关锁存器或门:所有调用await的线程都在门处等待,直到由调用countDown的线程打开它。

初始化为N的CountDownLatch可用于使一个线程等待到N个线程完成某个操作,或者某个操作已完成N次。

CountDownLatch的一个有用属性是,它不要求调用countDown的线程在继续之前等待计数为零,它只是阻止任何线程通过await,直到所有线程都可以通过。

CountDownLatch(int count)

构造一个用给定计数初始化的CountDownLatch

await()

导致当前线程等待,直到锁存器计数到零,除非线程被中断

countDown()

减少锁存器的计数,如果计数为零,则释放所有等待线程。

CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。

使用一个计数器进行实现。计数器初始值为线程的数量。

当每一个线程完成自己任务后,计数器的值就会减一。

当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。

应用场景

1、某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为new CountDownLatch(n),每当一个任务线程执行完毕,就将计数器减1【countdownLatch.countDown()】,当计数器的值变为0时,在CountDownLatch上await()的线程就会被唤醒。

一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。

2、实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的CountDownLatch(1),将其计算器初始化为1,多个线程在开始执行任务前首先countdownlatch.await(),当主线程调用countDown()时,计数器变为0,多个线程同时被唤醒。

使用CountDownLatch可以控制主线程最后执行

CyclicBarrier

同步帮助允许一组线程相互等待到达一个公共的障碍点。

cyclicbarrier在涉及固定大小的线程组的程序中非常有用,这些线程必须偶尔相互等待。

这个屏障之所以被称为循环的,是因为它可以在等待的线程被释放后被重用。

CyclicBarrier支持可选的Runnable命令,该命令在每个barrier点运行一次,在队列中的最后一个线程到达之后,但在任何线程释放之前。

这个屏障动作对于在任何一方继续之前更新共享状态是有用的。

CyclicBarrier的字面意思是可循环的(Cyclic)的使用屏障(Barrier)。它主要做的事情是让一组线程到达一个屏障点(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障是通过CyclicBarrier的await方法实现的。

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

当所有的线程都调用了 await方法,cyclicBarrier构造方法的线程才会执行,同时所有调用await方法的线程同时被唤醒,继续执行下面的操作

注意 :

CyclicBarrier的构造方法,可以传入一个实现了Runnable接口的类,表示,当所有的线程都调用了await,该线程才会被被执行。

CyclicBarrier的构造方法也可以只传入一个int类型的数字,表示当所有的线程(传入的参数数字的线程)都调用了await,所有的调用了await方法的线程都会被唤醒,继续执行下面的代码。

应用场景

交替打印foo bar

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class FooBar {
    private int n;

    private CyclicBarrier cb = new CyclicBarrier(2);
    volatile boolean fooExec = true;

    public FooBar(int n) {
        this.n = n;
    }

    public void foo(Runnable printFoo) throws InterruptedException {
        for (int i = 0; i < n; i++) {
            while (!fooExec) {
                //false的时候,bar线程在执行,foo线程在这此处空转
            }
            //打印foo
            printFoo.run();
            //设置变量
            fooExec = false;
            try {
                //线程foo到达同步点
                cb.await();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }

    public void bar(Runnable printBar) throws InterruptedException {
        for (int i = 0; i < n; i++) {
            try {
                cb.await();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
            printBar.run();
            fooExec = true;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //打印10次foo bar
        FooBar fooBar = new FooBar(10);
        Runnable printFoo = () -> {
            System.out.printf("%s\t", "foo");
        };
        Runnable printBar = () -> {
            System.out.printf("%s\n", "bar");
        };
        Thread fooThread = new Thread(() -> {
            try {
                fooBar.foo(printFoo);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread barThread = new Thread(() -> {
            try {
                fooBar.bar(printBar);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        fooThread.start();
        barThread.start();
    }
}

Semaphore

计数信号量。从概念上讲,信号量维护一组许可。如果有必要,每个人都可以获取区块,直到许可证可用,然后再获得它。每个版本添加一个许可,可能会释放一个阻塞的获取器。

但是,没有使用实际的permit对象;信号量只是保持可用数量的计数,并相应地进行操作。

Semaphore主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程的控制数量。

Semaphore是一种在多线程环境下使用的设施,该设施负责协调各个线程,以保证它们能够正确、合理的使用公共资源的设施,也是操作系统中用于控制进程同步互斥的量。Semaphore是一种计数信号量,用于管理一组资源,内部是基于AQS的共享模式。它相当于给线程规定一个量从而控制允许活动的线程数。

工作原理

以一个停车场运作为例。为了简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看门人允许其中三辆不受阻碍的进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入一辆,如果又离开两辆,则又可以放入两辆,如此往复。这个停车系统中,每辆车就好比一个线程,看门人就好比一个信号量,看门人限制了可以活动的线程。假如里面依然是三个车位,但是看门人改变了规则,要求每次只能停两辆车,那么一开始进入两辆车,后面得等到有车离开才能有车进入,但是得保证最多停两辆车。对于Semaphore类而言,就如同一个看门人,限制了可活动的线程数。

semaphore和synchronize的区别:

semaphore:可以控制多个线程进入,也可以是一个线程进入代码块

synchronize:只能控制一个线程计入代码块

应用场景

Semaphore主要用于控制当前活动线程数目,就如同停车场系统一般,而Semaphore则相当于看守的人,用于控制总共允许停车的停车位的个数,

而对于每辆车来说就如同一个线程,线程需要通过acquire()方法获取许可,而release()释放许可。

如果许可数达到最大活动数,那么调用acquire()之后,便进入等待队列,等待已获得许可的线程释放许可,从而使得多线程能够合理的运行。

控制线程的并发连接数

主要用于那些资源有明确访问数量限制的场景,常用于限流 。比如:数据库连接池,同时进⾏连接的线程有数量限制,连接不能超过⼀定的数量,当连接达到了限制数量后,后⾯的线程只能排队等前⾯的线程释放了数据库连接才能获得数据库连接。

你可能感兴趣的:(java)