30行自己写并发工具类(Semaphore, CyclicBarrier, CountDownLatch)是什么体验?
30行自己写并发工具类(Semaphore, CyclicBarrier, CountDownLatch)是什么体验?
前言
在本篇文章当中首先给大家介绍三个工具Semaphore, CyclicBarrier, CountDownLatch该如何使用,然后仔细剖析这三个工具内部实现的原理,最后会跟大家一起用ReentrantLock实现这三个工具。
并发工具类的使用
CountDownLatch
CountDownLatch最主要的作用是允许一个或多个线程等待其他线程完成操作。比如我们现在有一个任务,有\(N\)个线程会往数组data[N]当中对应的位置根据不同的任务放入数据,在各个线程将数据放入之后,主线程需要将这个数组当中所有的数据进行求和计算,也就是说主线程在各个线程放入之前需要阻塞住!在这样的场景下,我们就可以使用CountDownLatch。
上面问题的代码:
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
    public static int[] data = new int[10];
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            int temp = i;
            new Thread(() -> {
                Random random = new Random();
                data[temp] = random.nextInt(100001);
                latch.countDown();
            }).start();
        }
        // 只有函数 latch.countDown() 至少被调用10次
        // 主线程才不会被阻塞
        // 这个10是在CountDownLatch初始化传递的10
        latch.await();
        System.out.println("求和结果为:" + Arrays.stream(data).sum());
    }
}
在上面的代码当中,主线程通过调用latch.await();将自己阻塞住,然后需要等他其他线程调用方法latch.countDown()只有这个方法被调用的次数等于在初始化时给CountDownLatch传递的参数时,主线程才会被释放。
CyclicBarrier
CyclicBarrier它要做的事情是,让一 组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。我们通常也将CyclicBarrier称作路障。
示例代码:
public class CycleBarrierDemo {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(5);
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + "开始等待");
                    // 所有线程都会调用这行代码
                    // 在这行代码调用的线程个数不足5
                    // 个的时候所有的线程都会阻塞在这里
                    // 只有到5的时候,这5个线程才会被放行
                    // 所以这行代码叫做同步点
                    barrier.await();
                    // 如果有第六个线程执行这行代码时
                    // 第六个线程也会被阻塞 知道第10
                    // 线程执行这行代码 6-10 这5个线程
                    // 才会被放行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "等待完成");
            }).start();
        }
    }
}
我们在初始化CyclicBarrier对象时,传递的数字为5,这个数字表示只有5个线程到达同步点的时候,那5个线程才会同时被放行,而如果到了6个线程的话,第一次没有被放行的线程必须等到下一次有5个线程到达同步点barrier.await()时,才会放行5个线程。
- 比如刚开始的时候5个线程的状态如下,同步点还没有5个线程到达,因此不会放行。

- 当有5个线程或者更多的线程到达同步点barrier.await()的时候,才会放行5个线程,注意是5个线程,如果有多的线程必须等到下一次集合5个线程才会进行又一次放行,也就是说每次只放行5个线程,这也是它叫做CyclicBarrier(循环路障)的原因(因为每次放行5个线程,放行完之后重新计数,直到又有5个新的线程到来,才再次放行)。

Semaphore
Semaphore(信号量)通俗一点的来说就是控制能执行某一段代码的线程数量,他可以控制程序的并发量!
semaphore.acquire\(\mathcal{R}\)
semaphore.release比如在上面的
acquire和release之间的代码\(\mathcal{R}\)就是我们需要控制的代码,我们可以通过信号量控制在某一个时刻能有多少个线程执行代码\(\mathcal{R}\)。在信号量内部有一个计数器,在我们初始化的时候设置为\(N\),当有线程调用acquire函数时,计数器需要减一,调用release函数时计数器需要加一,只有当计数器大于0时,线程调用acquire时才能够进入代码块\(\mathcal{R}\),否则会被阻塞,只有线程调用release函数时,被阻塞的线程才能被唤醒,被唤醒的时候计数器会减一。
示例代码:
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class SemaphoreDemo {
    public static void main(String[] args) {
        Semaphore mySemaphore = new Semaphore(5);
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "准备进入临界区");
                try {
                    mySemaphore.acquire();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "已经进入临界区");
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "准备离开临界区");
                mySemaphore.release();
                System.out.println(Thread.currentThread().getName() + "已经离开临界区");
            }).start();
        }
    }
}
自己动手写并发工具类
在这一小节当中主要使用
ReentrantLock实现上面我们提到的三个并发工具类,因此你首先需要了解ReentrantLock这个工具。ReentrantLock中有两个主要的函数lock和unlock,主要用于临界区的保护,在同一个时刻只能有一个线程进入被lock和unlock包围的代码块。除此之外你还需要了解ReentrantLock.newCondition函数,这个函数会返回一个条件变量Condition,这个条件变量有三个主要的函数await、signal和signalAll,这三个函数的作用和效果跟Object类的wait、notify和notifyAll一样,在阅读下文之前,大家首先需要了解他们的用法。
- 哪个线程调用函数
condition.await,那个线程就会被挂起。- 如果线程调用函数
conditon.signal,则会唤醒一个被condition.await函数阻塞的线程。- 如果线程调用函数
conditon.signalAll,则会唤醒所有被condition.await函数阻塞的线程。
CountDownLatch
我们在使用CountDownLatch时,会有线程调用CountDownLatch的await函数,其他线程会调用CountDownLatch的countDown函数。在CountDownLatch内部会有一个计数器,计数器的值我们在初始化的时候可以进行设置,线程每调用一次countDown函数计数器的值就会减一。
- 如果在线程在调用await函数之前,计数器的值已经小于或等于0时,调用await函数的线程就不会阻塞,直接放行。
- 如果在线程在调用await函数之前,计数器的值大于0时,调用await函数的线程就会被阻塞,当有其他线程将计数器的值降低为0时,那么这个将计数器降低为0线程就需要使用condition.signalAll()函数将其他所有被await阻塞的函数唤醒。
- 线程如果想阻塞自己的话可以使用函数condition.await(),如果某个线程在进入临界区之后达到了唤醒其他线程的条件,我们则可以使用函数condition.signalAll()唤醒所有被函数await阻塞的线程。
上面的规则已经将CountDownLatch的整体功能描述清楚了,为了能够将代码解释清楚,我将对应的文字解释放在了代码当中:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class MyCountDownLatch {
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    private int curValue;
    public MyCountDownLatch(int targetValue) {
        // 我们需要有一个变量去保存计数器的值
        this.curValue = targetValue;
    }
    public void countDown() {
        // curValue 是一个共享变量
        // 我们需要用锁保护起来
        // 因此每次只有一个线程进入 lock 保护
        // 的代码区域
        lock.lock();
        try {
            // 每次执行 countDown 计数器都需要减一
            // 而且如果计数器等于0我们需要唤醒哪些被
            // await 函数阻塞的线程
            curValue--;
            if (curValue <= 0)
                condition.signalAll();
        }catch (Exception ignored){}
        finally {
            lock.unlock();
        }
    }
    public void await() {
        lock.lock();
        try {
            // 如果 curValue 的值大于0
            // 则说明 countDown 调用次数还不够
            // 需要将线程挂起 否则直接放行
            if (curValue > 0)
                // 使用条件变量 condition 将线程挂起
                condition.await();
        }catch (Exception ignored){}
        finally {
            lock.unlock();
        }
    }
}
可以使用下面的代码测试我们自己写的CountDownLatch:
public static void main(String[] args) throws InterruptedException {
    MyCountDownLatch latch = new MyCountDownLatch(5);
    for (int i = 0; i < 3; i++) {
        new Thread(() -> {
            latch.countDown();
            System.out.println(Thread.currentThread().getName() + "countDown执行完成");
        }).start();
    }
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            try {
                latch.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() +  "latch执行完成");
        }).start();
    }
}
CyclicBarrier
CyclicBarrier有一个路障(同步点),所有的线程到达路障之后都会被阻塞,当被阻塞的线程个数达到指定的数目的时候,就需要对指定数目的线程进行放行。
- 在CyclicBarrier当中会有一个数据threadCount,表示在路障需要达到这个threadCount个线程的时候才进行放行,而且需要放行threadCount个线程,这里我们可以循环使用函数condition.signal()去唤醒指定个数的线程,从而将他们放行。如果线程需要将自己阻塞住,可以使用函数condition.await()。
- 在CyclicBarrier当中需要有一个变量currentThreadNumber,用于记录当前被阻塞的线程的个数。
- 用户还可以给CyclicBarrier传入一个Runnable对象,当放行的时候需要执行这个Runnable对象,你可以新开一个线程去执行这个Runnable对象,或者让唤醒其他线程的这个线程执行Runnable对象。
根据上面的CyclicBarrier要求,写出的代码如下(分析和解释在注释当中):
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class MyCyclicBarrier {
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    private int threadCount;
    private int currentThreadNumber;
    private Runnable runnable;
    public MyBarrier(int count) {
        threadCount = count;
    }
    /**
     * 允许传入一个 runnable 对象
     * 当放行一批线程的时候就执行这个 runnable 函数
     * @param count
     * @param runnable
     */
    public MyBarrier(int count, Runnable runnable) {
        this(count);
        this.runnable = runnable;
    }
    public void await() {
        lock.lock();
        currentThreadNumber++;
        try {
            // 如果阻塞的线程数量不到 threadCount 需要进行阻塞
            // 如果到了需要由这个线程唤醒其他线程
            if (currentThreadNumber == threadCount) {
                // 放行之后需要重新进行计数
                // 因为放行之后 condition.await();
                // 阻塞的线程个数为 0
                currentThreadNumber = 0;
                if (runnable != null) {
                    new Thread(runnable).start();
                }
                // 唤醒 threadCount - 1 个线程 因为当前这个线程
                // 已经是在运行的状态 所以只需要唤醒 threadCount - 1
                // 个被阻塞的线程
                for (int i = 1; i < threadCount; i++)
                    condition.signal();
            }else {
                // 如果数目还没有达到则需要阻塞线程
                condition.await();
            }
        }catch (Exception ignored){}
        finally {
            lock.unlock();
        }
    }
}
下面是测试我们自己写的路障的代码:
public static void main(String[] args) throws InterruptedException {
    MyCyclicBarrier barrier = new MyCyclicBarrier(5, () -> {
        System.out.println(Thread.currentThread().getName() + "开启一个新线程");
        for (int i = 0; i < 1; i++) {
            System.out.println(i);
        }
    });
    for (int i = 0; i < 5; i++) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "进入阻塞");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            barrier.await();
            System.out.println(Thread.currentThread().getName() + "阻塞完成");
        }).start();
    }
}
Semaphore
Semaphore可以控制执行某一段临界区代码的线程数量,在Semaphore当中会有两个计数器semCount和curCount。
semCount表示可以执行临界区代码的线程的个数。
curCount表示正在执行临界区代码的线程的个数。
这个工具实现起来也并不复杂,具体分析都在注释当中:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class MySemaphore {
    private ReentrantLock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    private int semCount;
    private int curCount;
    public MySemaphore(int semCount) {
        this.semCount = semCount;
    }
    public void acquire() {
        lock.lock();
        try {
            // 正在执行临界区代码的线程个数加一
            curCount++;
            // 如果线程个数大于指定的能够执行的线程个数
            // 需要将当前这个线程阻塞起来
            // 否则直接放行
            if (curCount > semCount) {
                condition.await();
            }
        }catch (Exception ignored) {}
        finally {
            lock.unlock();
        }
    }
    public void release() {
        lock.lock();
        try {
            // 线程执行完临界区的代码
            // 将要离开临界区 因此 curCount
            // 需要减一
            curCount--;
            // 如果有线程阻塞需要唤醒被阻塞的线程
            // 如果没有被阻塞的线程 这个函数执行之后
            // 对结果也不会产生影响 因此在这里不需要进行
            // if 判断
            condition.signal();
            // signal函数只对在调用signal函数之前
            // 被await函数阻塞的线程产生影响 如果
            // 某个线程调用 await 函数在 signal 函数
            // 执行之后,那么前面那次 signal 函数调用
            // 不会影响后面这次 await 函数
        }catch (Exception ignored){}
        finally {
            lock.unlock();
        }
    }
}
使用下面的代码测试我们自己写的MySemaphore:
public static void main(String[] args) {
    MySemaphore mySemaphore = new MySemaphore(5);
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            mySemaphore.acquire();
            System.out.println(Thread.currentThread().getName() + "已经进入临界区");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            mySemaphore.release();
            System.out.println(Thread.currentThread().getName() + "已经离开临界区");
        }).start();
    }
}
总结
在本文当中主要给大家介绍了三个在并发当中常用的工具类该如何使用,然后介绍了我们自己实现三个工具类的细节,其实主要是利用条件变量实现的,因为它可以实现线程的阻塞和唤醒,其实只要大家了解条件变量的使用方法,和三种工具的需求大家也可以自己实现一遍。
以上就是本文所有的内容了,希望大家有所收获,我是LeHung,我们下期再见!!!(记得点赞收藏哦!)
更多精彩内容合集可访问项目:https://github.com/Chang-LeHung/CSCore
关注公众号:一无是处的研究僧,了解更多计算机(Java、Python、计算机系统基础、算法与数据结构)知识。
30行自己写并发工具类(Semaphore, CyclicBarrier, CountDownLatch)是什么体验?的更多相关文章
- 并发工具类——Semaphore
		本博客系列是学习并发编程过程中的记录总结.由于文章比较多,写的时间也比较散,所以我整理了个目录贴(传送门),方便查阅. 并发编程系列博客传送门 Semaphore([' seməf :(r)])的主要 ... 
- 并发工具类的使用 CountDownLatch,CyclicBarrier,Semaphore,Exchanger
		1.CountDownLatch 允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助. A CountDownLatch用给定的计数初始化. await方法阻塞,直到由于countDo ... 
- Java并发工具类Semaphore应用实例
		package com.thread.test.thread; import java.util.Random; import java.util.concurrent.*; /** * Semaph ... 
- j.u.c系列(09)---之并发工具类:CyclicBarrier
		写在前面 CyclicBarrier是一个同步辅助类,允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point).因为该 barrier 在释放等待线程后可以重用,所以 ... 
- JUC并发工具类之 CyclicBarrier同步屏障
		首先看看CyclicBarrier的使用场景: 10个工程师一起来公司应聘,招聘方式分为笔试和面试.首先,要等人到齐后,开始笔试:笔试结束之后,再一起参加面试.把10个人看作10个线程,10个线程之间 ... 
- Java多线程并发工具类-信号量Semaphore对象讲解
		Java多线程并发工具类-Semaphore对象讲解 通过前面的学习,我们已经知道了Java多线程并发场景中使用比较多的两个工具类:做加法的CycliBarrier对象以及做减法的CountDownL ... 
- JUC 常用4大并发工具类
		什么是JUC? JUC就是java.util.concurrent包,这个包俗称JUC,里面都是解决并发问题的一些东西 该包的位置位于java下面的rt.jar包下面 4大常用并发工具类: Count ... 
- 【重学Java】多线程进阶(线程池、原子性、并发工具类)
		线程池 线程状态介绍 当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态.线程对象在不同的时期有不同的状态.那么Java中的线程存在哪几种状态呢?Java中的线程 状态被定 ... 
- Java并发编程-并发工具类及线程池
		JUC中提供了几个比较常用的并发工具类,比如CountDownLatch.CyclicBarrier.Semaphore. CountDownLatch: countdownlatch是一个同步工具类 ... 
随机推荐
- 实现Linux系统的回收站
			Linux系统默认没有回收站功能,使用rm命令进行删除操作,文件就会直接从系统中删除,很难恢复. 今天我们利用简单的shell脚本实现Linux系统下的回收站机制. 先提供脚本代码 [root@qll ... 
- 干货 | Nginx负载均衡原理及配置实例
			一个执着于技术的公众号 Nginx系列导读 给小白的 Nginx 10分钟入门指南 Nginx编译安装及常用命令 完全卸载nginx的详细步骤 Nginx 配置文件详解 理解正向代理与反向代理的区别 ... 
- 万字长文深度剖析 RocketMQ 设计原理
			幸福的烦恼 张大胖最近是又喜又忧,喜的是业务量发展猛增,忧的是由于业务量猛增,一些原来不是问题的问题变成了大问题,比如说新会员注册吧,原来注册成功只要发个短信就行了,但随着业务的发展,现在注册成功也需 ... 
- Fluent-Validator 业务校验器
			Fluent-Validator 业务校验器 背景 在互联网行业中,基于Java开发的业务类系统,不管是服务端还是客户端,业务逻辑代码的更新往往是非常频繁的,这源于功能的快速迭代特性.在一般公司内部, ... 
- JavaMetaweblogClient,Metaweblog的java实现-从此上传博客实现全平台
			目录 1. 什么是Metaweblog? 2. Metaweblog的应用 3. 如何使用Metaweblog 4. 本项目介绍 4.1 metaweblog与java之间的关系映射 4.2 使用Ja ... 
- layui数据表格导入数据
			作为一个后端程序员,前端做的确实很丑,所以就学习了一下layui框架的使用.数据表格主要的问题就是传输数据的问题,这里我用我的前后端代码来做一个实际的分解. 前端部分 可以到layui官网示例中找到数 ... 
- 767. Reorganize String - LeetCode
			Question 767. Reorganize String Solution 题目大意: 给一个字符串,将字符按如下规则排序,相邻两个字符一同,如果相同返回空串否则返回排序后的串. 思路: 首先找 ... 
- 万字+28张图带你探秘小而美的规则引擎框架LiteFlow
			大家好,今天给大家介绍一款轻量.快速.稳定可编排的组件式规则引擎框架LiteFlow. 一.LiteFlow的介绍 LiteFlow官方网站和代码仓库地址 官方网站:https://yomahub.c ... 
- seafile私有网盘搭建
			各种公有网盘确实很方便,但总有些特殊情况不是? 闲来无聊准备自己搭建一个私有网盘,也让自己的闲置的服务器好好利用一下 搜索一番,找到了专业户seafile 一顿操作,踩了无数大坑,特此总结一下 1.c ... 
- LVS+keepalived高可用
			1.keeplived相关 1.1工作原理 Keepalived 是一个基于VRRP协议来实现的LVS服务高可用方案,可以解决静态路由出现的单点故障问题. 在一个LVS服务集群中通常有主服务器(MAS ... 
