zoukankan      html  css  js  c++  java
  • 信号量(Semaphore)、闭锁(Latch)、栅栏(Barrier)

    Semaphore、Barrier、Latch都属于同步工具类

    1、信号量(Semaphore)

    描述

    ​ 计数信号量用来控制同时访问某个特定资源的操作数量,或者同时执行某个制定操作的数量。还可以用来实现资源池。

    场景

    ​ 信号量就是一个计数器,所以应用很广泛。

    例如:我们构建一个有界队列,在队列满的时候希望阻塞而不是中断。那么信号量的大小就是队列的边界。

    例如:网络请求中,每秒只允许100个请求进入系统,超过100个则需要阻塞,直到有请求完成或释放信号。

    Semaphore

    介绍:计数信号量;

    主要方法:

    // 方法1:创建一个计数为bound的信号量
    Semaphore sem = new Semaphore(bound);
    // 方法2:获取一个信号量(如果sem中的计数已经为0,则此方法阻塞,直到sem中有空闲的信号量)
    sem.acquire();  
    // 方法3:释放一个信号量
    sem.release(); 
    

    使用方式:创建一个计数为n的信号量,然后当有请求或者有占用时,调用方法2拿走一个信号量。

    案例:使用信号量来维护set的大小。

    /**
     * 使用信号量(semaphore)构建一个有界容器。信号量的值为容器的大小
     * @Author: dhcao
     * @Version: 1.0
     */
    public class BoundedHashSet<T> {
        
        private final Set<T> set;
        private final Semaphore sem;
        
        public BoundedHashSet(int bound) {
            // 初始化信号量的值,作为set容器的边界
            this.set = Collections.synchronizedSet(new HashSet<T>());
            this.sem = new Semaphore(bound);
        }
    
        /**
         * 如果添加成功,则信号量的可用值减少一个;否则,被acquire占用的信号量要释放。
         * @param o
         * @return
         * @throws InterruptedException
         */
        public boolean add(T o) throws InterruptedException{
            
            // 1. 尝试取获取一个信号量,如果获取不到,此方法阻塞,直到能够获取到信号量
            sem.acquire();
            boolean wasAdded = false;
            
            try{
                wasAdded = set.add(o);
                return wasAdded;
            } finally {
                // 2. finaly会在return之前执行,但是不会改变return的值。
                if (!wasAdded) {
                    // 3. 如果添加失败,则set没有增加数据,则信号量不需要计数,所以释放它。
                    sem.release();
                }
            }
        }
    
        /**
         * 容器删除一个数据是,要释放对应的信号量
         * @param o
         * @return
         */
        public boolean remove(Object o){
            final boolean remove = set.remove(o);
    
            if (remove) {
                sem.release();
            }
            
            return remove;
        }
        
    }
    

    2、闭锁(Latch)

    描述

    ​ 闭锁是以一种同步工具类,它的作用相当于一扇门:在闭锁到达结束状态之前,这扇门一直关闭,并且没有任何线程能够通过,当达到结束状态时,这扇门会打开并允许所有线程通过。(不能被重置,终态不可逆)

    场景

    ​ 闭锁的用法很多

    例如:并发测试中,我们希望2000个线程同时开始执行。那么闭锁的结束状态:线程数目 = 2000。

    例如:资源加载中,我们希望所有菜单加载完毕后才允许访问。那么闭锁的结束状态:所有菜单已加载。

    例如:价格对比中,商品A的价格从3个渠道获取,3个渠道均返回报价之后选择最优渠道。那么闭锁的结束状态:所有渠道均返回报价。

    CountDownLatch

    介绍:顾名思义,这是一个计数器闭锁。

    主要方法:

    // 方法1:构造函数,计数器为n
    CountDownLatch startGate = new CountDownLatch(int n);
    // 方法2:线程等待
    startGate.await();
    // 方法3:计数减1
    startGate.countDown();
    

    使用方式:线程调用方法2则开始等待,直到线程startGate 计数器为0;

    案例1:创建n条线程,并让他们并发执行(同时开始,就像跑步一样,所有人就位之后同时开始)

    	/**
         * nThreads条线程同时开始执行任务task
         * @param nThreads 线程数量
         * @param task 需要执行的任务
         * @return 
         * @throws InterruptedException 线程中断异常交由客户端处理
         */
        public static void timeTasks(int nThreads, final Runnable task) throws InterruptedException{
    
            final CountDownLatch startGate = new CountDownLatch(1);
    
            for (int i = 0; i < nThreads; i++) {
                Thread t = new Thread(new Runnable() {
                    public void run() {
                        try {
                            // 1. 每条线程出来都开始等待;for循环共创建nThread条线程,线程就绪之后等待信号
                            startGate.await();
                            task.run();
                        }catch (InterruptedException ignored){
    
                        }
                    }
                });
                t.start();
            }
    		
            // 2.startGate - 1 = 0;在1中等待的线程开始执行。即已经准备好的nThreads条线程同时开始执行
            startGate.countDown();
        }
    

    案例2: 计算nThreads条线程并发执行任务的时间

    	/**
         * 测试并发执行任务的时间(包含了创建线程的时间)
         * @param nThreads 线程数量
         * @param task 需要执行的任务
         * @return 任务完成时间
         * @throws InterruptedException 线程中断异常交由客户端处理
         */
        public static long timeTasks(int nThreads, final Runnable task) throws InterruptedException{
    
            final CountDownLatch startGate = new CountDownLatch(1);
            final CountDownLatch endGate = new CountDownLatch(nThreads);
    
            for (int i = 0; i < nThreads; i++) {
                Thread t = new Thread(new Runnable() {
                    public void run() {
    
                        try {
                            // 1. 每条线程出来都开始等待;for循环共创建nThread条线程,所有
                            startGate.await();
                            try{
                                task.run();
                            }finally {
                                // 2. 每条线程完成run任务之后,先等待着,直到nThreads线程都完成;
                                endGate.countDown();
                            }
                        }catch (Exception ignored){
    
                        }
                    }
                });
                t.start();
            }
    
            long start = System.nanoTime();
            
            // 3.所有线程准备好之后,统一放行
            startGate.countDown();
            // 4.nThreads条线程都完成任务之后,endGate统计最后时间!
            endGate.await();
            long end = System.nanoTime();
    
            return end - start;
        }
    

    上述案例在互联网项目中做并发测试时很有意义,所以选择了这2个例子。通常测试并发时,如果只是for循环创建线程再执行,其实线程还是有先后顺序的。如果使用Executor框架,代码又显得过于复杂,所以我常使用上述案例来做并发测试。

    3、栅栏(Barrier)

    描述

    ​ 栅栏,顾名思义,是一个拦截屏障,类似于闭锁,它能阻塞一组线程直到某个事件发生。栅栏与闭锁的关键区别在于,所有线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件,而栅栏用于等待其他线程。

    场景

    ​ 栅栏应用于很多依赖性场景

    例如:考试时当所有考生都交卷之后,才能开始阅卷。

    例如:约定所有人都到达公司楼下,才发车去郊游。

    例如:游戏加载中(lol或者dota??),我们希望所有玩家都就绪之后才开始游戏。

    CyclicBarrier

    介绍:顾名思义,这是一个循环屏障。它可以循环运行(区别于CountDownLatch终态之后不可复用)

    主要方法:

    // 方法1:创建一个栅栏;run为栅栏拦截的线程到齐之后进行的操作;n 为线程数目
    CyclicBarrier barrier = new CyclicBarrier(int n, Runnable run);
    // 方法2:被此栅栏约束的线程在等待
    barrier.await();
    

    使用方式:创建一个栅栏,然后将此栅栏传递给线程,代表此类型线程要被该栅栏约束。

    案例

    /**
     * 5个同事去吃饭,约定在饭店门口见面
     * @Author: dhcao
     * @Version: 1.0
     */
    public class BarrierUtil {
    
        /**
         * 定义同事类
         */
        private static class Colleague extends Thread{
    
            // 要约定栅栏;所有的同事实现类都要被此栅栏约束
            private CyclicBarrier barrier;
    
            Colleague(CyclicBarrier barrier, String name){
                super(name);
                this.barrier = barrier;
            }
    
    
            @Override
            public void run() {
    
                try {
    
                    System.out.println("同事:" + getName() + " 已经到达约定地点,等待其他人");
                    barrier.await();
                    System.out.println("人齐了,去吃饭");
    
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        }
    
    
        public static void main(String[] args) {
            int colleagueNum = 5;
            // 1. 定义一个栅栏:5条线程到位之后执行run方法(由最后到达的线程执行)
            CyclicBarrier barrier = new CyclicBarrier(colleagueNum, new Runnable() {
                @Override
                public void run() {
                    System.out.println("执行此操作的线程:" + Thread.currentThread().getName() + "。所有人均已到达约定地点;领头进门....");
                }
            });
    
            // 2. 将此栅栏约束在线程上(如果i<其他值)
            for (int i = 0; i < colleagueNum; i++) {
                new Colleague(barrier,"同事" + i).start();
            }
        }
    }
    
  • 相关阅读:
    【JAVA零基础入门系列】Day3 Java基本数据类型
    【JAVA零基础入门系列】Day2 Java集成开发环境IDEA
    【JAVA零基础入门系列】Day1 开发环境搭建
    易语言 【寻找文本】命令的bug
    类的进化史
    C++指针类型识别正确姿势
    C++ 编写DLL文件给易语言调用
    C++中的显式类型转化
    CC++ 1A2B小游戏源码
    C语言dll文件的说明以及生成、使用方法
  • 原文地址:https://www.cnblogs.com/dhcao/p/11710660.html
Copyright © 2011-2022 走看看