Java 进阶——并发编程之线程同步利器CountDownLatch、CyclicBarrier、Semaphore 的使用小结

引言

Java 语言之所以广泛运用于服务端程序,很大一部分原因就是因为在JDK中Java 已经为我们提供了很多并发场景的解决方案,借助这些系统方案我们可以快速应用于具体场景,甚至是在系统方案上进行扩展,这篇文章就好好总结下三种线程控制工具类。

一、CountDownLatch

1、CountDownLatch概述

CountDownLatch 是一种允许一个或多个线程阻塞等待,直到在其他线程中的一系列操作完成之后,再继续执行的一次性同步机制(A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes)。简单理解CountDownLatch(内部是基于AbstractQueuedSynchronizer,篇幅问题不在此篇文章分析范围内)就是一个基于信号量机制的阻塞和唤醒线程的工具类,通过调用CountDownLatch提供的api方法就可以达到灵活阻塞和唤醒线程的目的。所谓信号量,其实和引用计数技术中的差不多,所以在初始化的时候需要给这个信号量赋一个数值(一般大于0),每调用CountDownLatch的countDown方法一次,信号量值减 1;若信号量当前值不为0时,调用await()方法的线程会被阻塞,直到count值为0才会继续执行;反之则不会阻塞。另外,CountDownLatch不可复用,计数不能重置

2、CountDownLatch的主要方法

方法名称 说明
public CountDownLatch(int count) 通过指定信号量值构造CountDownLatch
public void await() 调用await()方法的线程会被阻塞,直到count值为0才继续执行
public boolean await(long timeout,TimeUnit unit) 和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public void countDown() 将count值减1

3、CountDownLatch的简单应用

众所周知,即使我们几乎同时创建出若干条子线程,且每个子线程内部执行的工作一样,也不能保证每个子线程运行的顺序和结束的时间,因为启动过程和执行过程需要靠cpu 来调度,下面这个例子每次运行的结果也有所不同,但是基本上通过CountDownLatch都能保证主线程在每一组的所有子线程执行完毕之后再继续执行,总之调用await方法时后且当前信号量值不为0 调用线程则会被阻塞,而且一个线程可以被一个或者多个CountDownLatch控制。

  • 通过CountDownLatch(int count)初始化创建CountDownLatch实例
  • 根据具体场景在某个线程中调用await()或者await(long timeout,TimeUnit unit)方法尝试阻塞该线程
  • 在线程内部或者其他线程调用 countDown()对计数减一
    public class CountDownLatchDemo {

        private final static int GROUP_SIZE = 3;

        public static void main(String[] args) throws InterruptedException {
            processOneGroup(第一组);
        }

        /**
         * 
         * @param name 
         * @throws InterruptedException
         */
        private static void processOneGroup(final String name)
                throws InterruptedException {
            final CountDownLatch mainCountDown = new CountDownLatch(GROUP_SIZE);//用于控制主线程的CountDownLatch
            final CountDownLatch startCountDown = new CountDownLatch(1);//

            System.out.println("***首先"+Thread.currentThread().getName()+"线程运行:" + "通过循环创建若干工作线程" + "***");

            for (int i = 0; i < GROUP_SIZE; i++) {
                new Thread(String.valueOf(i)) {
                    public void run() {

                        System.out.println(name + "的" + getName() + "线程已经准备就绪");
                        try {
                            startCountDown.countDown();
                            startCountDown.await();//如果startCountDown初始信号量值为1的话经过上一步的coutDown 当前信号量值已经为0 了 即使调用了await也无法阻塞
                            System.out.println("先调用await方法阻塞"+getName()+ "线程");
                            System.out.println(getName()+ "线程处理自己的工作ing");
                            Thread.sleep(3000);
                            mainCountDown.countDown();//主线程计数减一
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }

                        System.out.println("子线程"+Thread.currentThread().getName() + "运行:"+name + "的" + getName() + "线程已执行完毕");

                    }
                }.start();
            }

        mainCountDown.await();// 等待所有子线程执行完毕

        System.out.println("线程"+Thread.currentThread().getName() + "运行:"+name + "中所有工作线程工作完毕");
        //startCountDown.countDown();
        System.out.println("----end线程"+Thread.currentThread().getName() + "继续执行------");
    }

这里写图片描述

二、CyclicBarrier

1、CyclicBarrier概述

CyclicBarrier首先从字面上理解是Cyclic Barrier 即循环屏障。CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;而且CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。简而言之,CyclicBarrier可以让一组线程阻塞(暂且把这个状态就叫做barrier)直到barrier状态之后再全部(全部指的是getParties 获取到的所有等待线程)同时执行,当某个线程调用await()方法之后,该线程就处于barrier了。

2、CyclicBarrier的主要方法

方法名称 说明
public CyclicBarrier(int parties) parties:必须要调用await方法的线程数,
public CyclicBarrier(int parties, Runnable barrierAction) barrierAction:clicBarrier到达 barrier状态时候要执行才操作
public void await() 调用await()方法的线程会被阻塞,直到CyclicBarrier上所有的线程(Waits until all getParties parties have invoked await on this barrier)都执行了await方法
public boolean await(long timeout,TimeUnit unit) 和await()类似,让这些线程等待至一定的时间,如果还有线程没有到达barrier状态,也直接让到达barrier的线程执行后续任务。
public void reset() 重用CyclicBarrier,将CyclicBarrier状态置为初始态,若当前还有其他线程在阻塞则会抛出BrokenBarrierException异常

3、CyclicBarrier的简单应用

3.1、CyclicBarrier的基本应用

package concurrent;

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

public class CyclicBarrierDemo {
    private static final int THREAD_COUNT=3;
    /**
     * 如果这里parties 不等于THREAD_COUNT会出现问题,可能会一直卡住
     */
    private final static CyclicBarrier CYCLIC_BARRIER=new CyclicBarrier(THREAD_COUNT, new Runnable() {

        @Override
        public void run() {
            System.out.println("全部团员已经结束上次行程,导游开始点名,准备进行下一个环节");
        }
    });


    public static void main(String[] args) {
        for(int i=0;i<3;i++){
            new Thread(String.valueOf(i)){
                public void run(){

                    try {
                        System.out.println("线程"+getName()+" 已到达旅游地点!");
                        CYCLIC_BARRIER.await();//阻塞当前线程

                        System.out.println("线程"+getName()+" 开始骑车");
                        Thread.sleep(2000);
                        CYCLIC_BARRIER.await();
                        System.out.println("线程"+getName()+" 开始爬山");
                        Thread.sleep(3000);
                        CYCLIC_BARRIER.await();
                        System.out.println("线程"+getName()+" 回宾馆休息");
                        Thread.sleep(1000);
                        CYCLIC_BARRIER.await();

                        System.out.println("线程"+getName()+" 开始乘车回家");
                        CYCLIC_BARRIER.await();
                        System.out.println("线程"+getName()+" 回家了");

                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        System.out.print(e.getMessage());
                        e.printStackTrace();
                    }   
                }
            }.start();
        }
    }
}

这里写图片描述

3.2、CyclicBarrier的重用

package concurrent;

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

public class ReuseCyclicBarrier {
    public static void main(String[] args) {
        final int THREAD_COUNT=3;
        CyclicBarrier barrier  = new CyclicBarrier(THREAD_COUNT, new Runnable() {

            @Override
            public void run() {
                print("所有工作线程都已到达barrier状态");
            }
        });

        print("*******第一组 使用 CyclicBarrier*********");
        for(int i=0;i<THREAD_COUNT;i++) {
            new Worker(barrier).start();
        }

        try {
            Thread.sleep(9000);//此处为了保证主线程被阻塞直到第一组3个线程执行完毕所有的工作,所以至少要sleep 3000*3,如果少于则不会等到其他三个线程执行完毕之后才执行主线程
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        print("*************第二组 重用 CyclicBarrier***************\n");
        //barrier.reset(); Resets the barrier to its initial state.  If any parties are currently 
        //waiting at the barrier, they will return with a BrokenBarrierException
        barrier  = new CyclicBarrier(THREAD_COUNT-1, new Runnable() {

                    @Override
                    public void run() {
                        print("【重用】所有工作线程都已到达barrier状态");
                    }
                });
        for(int i=0;i<THREAD_COUNT;i++) {
            new Worker(barrier).start();
        }
    }
    static class Worker extends Thread{
        private CyclicBarrier cyclicBarrier;
        public Worker(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }

        @Override
        public void run() {

            try {
                doWork(3000L);     
                print("操作数据完毕,等待其他工作线程"+"cyclicBarrier中的Parties数"+cyclicBarrier.getParties());

                cyclicBarrier.await();//只是会阻塞,如果这里耗时很短的话,并不会阻塞主线程
            }catch(BrokenBarrierException | InterruptedException e){
                e.printStackTrace();
            }
            print("所有工作线程都已到达barrier状态后,所有线程执行操作完毕,继续处理其他任务...");
        }
    }

    public static void print(String s) {
        System.out.println(Thread.currentThread().getName()+" 线程 :  "+s);
    }

    public static void doWork(long mills){
        try {
            print("正在操作数据ing");
            Thread.sleep(mills);//以睡眠来模拟数据操作
        } catch (InterruptedException e) {
            e.printStackTrace();
        } 
    }
}

这里写图片描述

三、Semaphore

1、Semaphore 概述

Semaphore首先从字面上理解是 信号量Semaphore可以控制同时访问某组资源的线程个数,Semaphore当前在多线程环境下被广泛使用,操作系统的信号量是个很重要的概念,在进程控制方面都有应用。Java 并发库 的Semaphore 可以很轻松完成信号量控制可以通过 acquire() 方法获取一个许可,如果没有就阻塞;而 release() 方法释放一个许可,其实Semaphore和锁机制有点类似,信号量一般用于控制对 某组 资源的访问权限,而是控制对 某个 资源的访问权限

2、Semaphore 的主要方法

方法名称 说明
public Semaphore(int permits) permits:一组资源最大同时可以被permits个线程所访问
public Semaphore(int permits, boolean fair) fair:和阻塞队列一样用于标识是否采用公平的访问优先策略,true:按照先来后到的顺序获得机会;false:随机获得优先机会
public void acquire() 获取一个许可,若无许可能够获得,则会一直阻塞,直到获得许可。
public void acquire(int permits) 获取permits个许可
public void release() 释放一个许可,在释放许可之前,必须先获获得许可
public void release(int permits) 释放permits个许可
以上四种方法都会阻塞,下面tryXXX的不会阻塞——————
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

3、Semaphore 的简单应用

举个例子每一层楼指纹打卡机只有3台,而打卡的员工远不止,那么为了更高效率,只得确保同时只能有3个人能够使用,而当这3个人中的任何一个人打卡完毕离开后,剩下正在等待的其他人中马上又有1个人可以使用了(这个人可以是等待队伍中的任意一个,可以是随机获得优先机会,也可以是按照先来后到的顺序获得机会)这取决于构造Semaphore对象时传入的参数选项。另外单个信号量的Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程获得了“锁”,再由另一个线程释放“锁”,这可应用于死锁恢复的一些场合。

package concurrent;


import java.util.Random;
import java.util.concurrent.Semaphore;

/**
 * 假如一个工厂有3台打卡机,但是有6名员工,一台机器同时只能被一个员工使用,只有使用完了,其他员工才能继续使用
 * @author Crazy.Mo
 *
 */
public class SemaphoreTest {
    public static void main(String[] args) {
        final int WORKER_NUM = 6;            //总员工数
        final int SOURCE_COUNT=3;//3台指纹打卡机,一组资源
        Semaphore semaphore = new Semaphore(SOURCE_COUNT); //最大同时允许SOURCE_COUNT 个员工使用

        for(int i=0;i<WORKER_NUM;i++){
            new FingerPrint(i,semaphore).start();
        }
    }

    static class FingerPrint extends Thread{
        private int num;
        private Semaphore semaphore;
        public FingerPrint(int num,Semaphore semaphore){
            this.num = num;
            this.semaphore = semaphore;
        }

        @Override
        public void run() {
            try {
                semaphore.acquire();
                print("员工"+this.num+"正在使用一台指纹打卡机ing。。。");
                Thread.sleep(new Random().nextInt(3000));
                print("员工"+this.num+"释放出一台指纹打卡机");
                semaphore.release();           
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void print(String s) {
        System.out.println(Thread.currentThread().getName()+" 线程 :  "+s);
    }

    public static void doWork(long mills){
        try {
            Thread.sleep(mills);//以睡眠来模拟打卡
        } catch (InterruptedException e) {
            e.printStackTrace();
        } 
    }
}

这里写图片描述

猜你喜欢

转载自blog.csdn.net/CrazyMo_/article/details/81221257