文章目录

  • CountDownLatch(线程计数器)
  • CyclicBarrier(循环屏障)
  • Semaphore(信号量)
  • volatile 关键字的作用



CountDownLatch(线程计数器)

  1. CountDownLatch 类位于 java.util.concurrent 包下,利用它可以实现类似计数器的功能。比如有一个任务 A,它要等待其他 4 个任务执行完毕之后才能执行,此时就可以利用 CountDownLatch来实现这种功能了。
  2. CountDownLatch基于线程计数器来实现并发访问控制,主要用于主线程等待其他子线程都执行完毕后执行相关操作。其使用过程为:在主线程中定义CountDownLatch,并将线程计数器的初始值设置为子线程的个数,多个子线程并发执行,每个子线程在执行完毕后都会调用countDown函数将计数器的值减1,直到线程计数器为0,表示所有的子线程任务都已执行完毕,此时在CountDownLatch上等待的主线程将被唤醒并继续执行。
  3. 有一个主线程,它需要等待其他两个任务都执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能。
public class Test {
    public static void main(String[] args) {
       //定义大小为2的CountDownLatch
        final CountDownLatch latch = new CountDownLatch(2);

        new Thread(){
            @Override
            public void run() {
                try {
                    System.out.println("子线程1 is running!");

                    Thread.sleep(3000);
                    System.out.println("子线程1 执行完毕");
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                try {
                    System.out.println("子线程2 is running!");

                    Thread.sleep(3000);
                    System.out.println("子线程2 执行完毕");
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();

        try{
            System.out.println("等待两个子线程执行完毕....");
            latch.await();
            System.out.println("两个子线程已经执行完毕,继续执行主线程");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

运行结果:

子线程1 is running!
等待两个子线程执行完毕....
子线程2 is running!
子线程1 执行完毕
子线程2 执行完毕
两个子线程已经执行完毕,继续执行主线程
  1. 小结
    (1)定义好CountDownLatch的大小final CountDownLatch latch = new CountDownLatch(2); (2)子线程在操作完后,加上代码latch.countDown(); (3)主线程要等待子线程完成后执行,在执行前加上latch.await();

CyclicBarrier(循环屏障)

  1. CyclicBarrier(循环屏障)是一个同步工具,可以实现让一组线程等待至某个状态之后再全部同时执行。在所有等待线程都被释放之后,CyclicBarrier可以被重用。
  2. CyclicBarrier的运行状态叫作Barrier状态,在调用await方法后,线程就处于Barrier状态。
  3. CyclicBarrier中最重要的方法是await方法,它有两种实现。

(1)public int await():挂起当前线程直到所有线程都为Barrier状态再同时执行后续的任务。

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

(2)public int await(long timeout, TimeUnit unit):设置一个超时时间,在超时时间过后,如果还有线程未达到Barrier状态,则不再等待,让达到Barrier状态的线程继续执行后续的任务。

public int await(long timeout, TimeUnit unit)
        throws InterruptedException,
               BrokenBarrierException,
               TimeoutException {
        return dowait(true, unit.toNanos(timeout));
    }
  1. 具体使用:

(1)先定义了一个CyclicBarrier,然后循环启动了多个线程,每个线程都通过构造函数将CyclicBarrier传入线程中,在线程内部开始执行第1阶段的工作,比如查询数据等;
(2)等第1阶段的工作处理完成后,再调用cyclicBarrier.await方法等待其他线程也完成第1阶段的工作(CyclicBarrier让一组线程等待到达某个状态再一起执行);
(3)等其他线程也执行完第1阶段的工作,便可执行并发操作的下一项任务。
(4)代码实现:

public class BusinessThread extends Thread{

    private CyclicBarrier cyclicBarrier;

    public BusinessThread(CyclicBarrier cyclicBarrier) {
        this.cyclicBarrier = cyclicBarrier;
    }


    @Override
    public void run() {

        try {
            //执行业务线程逻辑,这里sleep
            Thread.sleep(5000);

            System.out.println("线程执行前准备工作完成,等待其他线程准备工作完成");

            //这个线程已经完成==》等待其他线程也进入Barrier状态
            cyclicBarrier.await();
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println("所有准备工作都已完成,执行下一项任务");
        
        //下一阶段工作的代码
    }
}
public class Test02 {
    public static void main(String[] args) {
        int n = 4;

        CyclicBarrier barrier = new CyclicBarrier(n);
        for (int i = 0; i < n; i++) {
            new BusinessThread(barrier).start();
        }
    }
}

运行结果:我们执行了4个线程,4个线程的执行前的准备工作都完成了才可以执行每个线程的第二项任务。

线程执行前准备工作完成,等待其他线程准备工作完成
线程执行前准备工作完成,等待其他线程准备工作完成
线程执行前准备工作完成,等待其他线程准备工作完成
线程执行前准备工作完成,等待其他线程准备工作完成
所有准备工作都已完成,执行下一项任务
所有准备工作都已完成,执行下一项任务
所有准备工作都已完成,执行下一项任务
所有准备工作都已完成,执行下一项任务

Semaphore(信号量)

  1. Semaphore信号量:用于控制同时访问的线程个数,通过acquire() 获取一个许可,如果没有就等待,在许可使用完毕后通过 release() 释放该许可。
  2. Semaphore常被用于多个线程需要共享有限资源的情况。
  3. 具体实例:办公室有两台打印机,但是有5个员工需要使用,一台打印机同时只能被一个员工使用,其他员工排队等候,且只有该打印机被使用完毕并释放后其他员工方可使用。

(1)首先定义好员工打印操作线程类:

public class Worker extends Thread{
    private String name;
    private Semaphore semaphore;

    public Worker(String name,Semaphore semaphore) {
        super(name);
        this.semaphore = semaphore;
    }

    @Override
    public void run() {
        try {
            //线程申请资源  ===》打印机
            semaphore.acquire();

            System.out.println(Thread.currentThread().getName()+"    占用一个打印机......");

            //打印操作....
            Thread.sleep(5000);

            //打印完毕 释放资源
            System.out.println(Thread.currentThread().getName()+"    打印完毕,释放出打印机");
            semaphore.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

(2) 测试类:

public class Test03 {

    public static void main(String[] args) {
        //5个员工===》5个线程
        int printNumber = 5;

        //2台打印机===》并发数为2
        Semaphore semaphore = new Semaphore(2);


        for (int i = 0; i < printNumber; i++) {
            new Worker("员工"+i,semaphore).start();
        }
    }
}

运行结果:

员工0    占用一个打印机......
员工1    占用一个打印机......
员工0    打印完毕,释放出打印机
员工1    打印完毕,释放出打印机
员工2    占用一个打印机......
员工4    占用一个打印机......
员工2    打印完毕,释放出打印机
员工4    打印完毕,释放出打印机
员工3    占用一个打印机......
员工3    打印完毕,释放出打印机
  1. 小结

(1)通过构造函数将Semaphore传入线程内部。在线程调用semaphore.acquire()时开始申请许可并执行业务逻辑,在线程业务逻辑执行完成后调用semaphore.release()释放许可以便其他线程使用。
(2)在Semaphore类中比较重要的方法

  • public void acquire():以阻塞的方式获取一个许可,在有可用许可时返回该许可,在没有可用许可时阻塞等待,直到获得许可。
  • public void acquire(int permits):同时获取permits个许可。
  • public void release():释放某个许可。
  • public void release(int permits):释放permits个许可。
  • public boolean tryAcquire():以非阻塞方式获取一个许可,在有可用许可时获取该许可并返回true,否则返回false,不会等待。
  • public boolean tryAcquire(long timeout, TimeUnit unit):如果在指定的时间内获取到可用许可,则返回true,否则返回false。
  • public boolean tryAcquire(int permits):如果成功获取permits个许可,则返回true,否则立即返回false。
  • public boolean tryAcquire(int permits, long timeout, TimeUnit unit):如果在指定的时间内成功获取permits个许可,则返回true,否则返回false。
  • availablePermits():查询可用的许可数量。

(3)CountDownLatch、CyclicBarrier、Semaphore的区别:

  • CountDownLatch 和 CyclicBarrier 都能够实现线程之间的等待,只不过它们侧重点不同
  • CountDownLatch 一般用于某个线程 A 等待若干个其他线程执行完任务之后,它才执行
  • CyclicBarrier 一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行
  • CountDownLatch 是不能够重用的,而 CyclicBarrier 是可以重用的
  • Semaphore 其实和锁有点类似,它一般用于控制对某组资源的访问权限。

volatile 关键字的作用

  1. Java除了使用了synchronized保证变量的同步,还使用了稍弱的同步机制,即volatile变量。volatile也用于确保将变量的更新操作通知到其他线程。
  2. volatile变量具备两种特性:
  • 一种是保证该变量对所有线程可见,在一个线程修改了变量的值后,新的值对于其他线程是可以立即获取的;
  • 一种是volatile禁止指令重排,即volatile变量不会被缓存在寄存器中或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
  1. 在访问 volatile 变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此 volatile 变量是一种比 sychronized 关键字更轻量级的同步机制。volatile 适合这种场景:一个变量被多个线程共享,线程直接给这个变量赋值。
  2. 普通变量和volatile修饰的变量

(1)普通变量:

在有多个线程对普通变量进行读写时,每个线程都首先需要将数据从内存中复制变量到CPU缓存中,如果计算机有多个CPU,则线程可能都在不同的CPU中被处理,这意味着每个线程都需要将同一个数据复制到不同的CPU Cache中,这样在每个线程都针对同一个变量的数据做了不同的处理后就可能存在数据不一致的情况

java中count是 java的count_java中count是

(2)volatile修饰的变量
如果将变量声明为volatile, JVM就能保证每次读取变量时都直接从内存中读取,跳过CPU缓存这一步,有效解决了多线程数据同步的问题。

java中count是 java的count_volatile_02

  1. 小结

(1)volatile 变量的单次读/写操作可以保证原子性的。
(2)不能保证 i++这种操作的原子性,因为本质上 i++是读、写两次操作。
(3)在某些场景下可以代替 Synchronized。但是,volatile 的不能完全取代 Synchronized 的位置,只有在一些特殊的场景下,才能适用 volatile。
(4)必须同时满足下面两个条件才能保证在并发环境的线程安全:

  • 对变量的写操作不依赖于当前值(比如i++),或者说是单纯的变量赋值(boolean flag = true)。
  • 该变量没有被包含在具有其他变量的不变式中,也就是说在不同的volatile变量之间不能互相依赖,只有在状态真正独立于程序内的其他内容时才能使用volatile

(5)volatile关键字的使用方法比较简单,直接在定义变量时加上volatile关键字即可:

volatile boolean flag = false;