信号量、闭锁、栅栏、栅栏闭锁(java)

  

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)

描述

 闭锁(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();
        }
    }
}
栅栏闭锁

栅栏闭锁是将闭锁和栅栏的特性结合在一起,用于让一组线程在达到某个点后,互相等待,等待所有线程都就绪后再一起继续执行。

例如,假设有一个比赛,所有运动员都在起跑线上就绪后,裁判员发出比赛开始信号,所有运动员同时出发。这里,裁判员就是栅栏,起跑线上的运动员是闭锁。裁判员等待所有运动员到达起跑线,然后发出信号开始比赛。

在代码中,可以使用 CyclicBarrier 控制所有线程到达某个点后再同时执行。这个点可以是某个特定任务的起点,一旦所有线程都到达这个点,就可以执行这个任务。下面是一个简单的示例:

java   
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
  public class Race {
      public static void main(String[] args) {
          int numberOfRunners = 5;
          CyclicBarrier startBarrier = new CyclicBarrier(numberOfRunners);
          for (int i = 0; i < numberOfRunners; i++) {
            new Thread(() -> {
              try {
                  System.out.println("Runner is waiting at the starting line");
                  startBarrier.await(); // 等待所有运动员到达起跑线
                  System.out.println("Runner started running");
                  }catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                  }
              }).start();
      }
    }
}

在这个例子中,所有的运动员(线程)都等待在起跑线上,然后当所有运动员都到达起跑线时,他们会同时开始比赛。这个例子中的 CyclicBarrier 就是栅栏,线程等待在起跑线就是闭锁,它们结合起来实现了栅栏闭锁的效果。

 
posted @ 2023-11-15 19:18  qunqingrihe  阅读(129)  评论(1编辑  收藏  举报