美文网首页微服务架构
Java并发|Semaphore如何实现限流器

Java并发|Semaphore如何实现限流器

作者: 程序员七哥 | 来源:发表于2020-10-25 00:53 被阅读0次

    大家好,我是七哥,今天是2020.10.24,也是我们程序员的节日,在这里祝大家节日快乐。

    絮叨一下

    今天我们一起来学习下如何使用JDK提供的并发工具类来实现限流。在之前的工作中,我们有一个限流的场景,那就是在调用关联方系统的时候需要限流,因为提供服务方是保险的核心系统,大家应该都懂这种系统支持的并发不会大,为了保护双方系统的可用性,作为调用方我们在调用的时候也会做一个限流控制。这种场景在工作中很常见,之前面试的时候也经常会被问到:「Java并发包你有哪些使用场景?」

    你可能已经想到了,我们当时的解决方案就是使用信号量 Semaphore, 由于是JDK并发包中提供的工具类,也不用引入第三方包,实现非常的简单。这里要注意的是,我们当时的服务是分布式集群部署,而 信号量只能保存单机的并发控制,也就是必须在一个JVM进程中,所以我们是用服务提供方支持的最大并发数除以我们的机器数量来为每台实例配置限流的。

    信号量简介

    信号量在《Java并发编程的艺术》一书中是这样定义的:

    Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。

    看了上面的描述对于信号量的定义其实还是不好理解,我来举个例子帮助大家记忆,起码我是这样记住的。

    我们可以将信号量理解为信号灯,也就是我们生活中的红绿灯,假设早晚高峰主干道需要限流,同一时间只允许10辆车在当前道路上行驶,那么其他车辆都必须在路口等待,所以前10辆车看到的就是绿灯可以通行,后面的车看到的就是红灯不能驶入此道路,但是如果有2辆车已经驶出这条道路,那么后面的车辆中就又可以允许两辆车驶入。

    在这个例子中,车辆就是线程,驶入马路就是表示线程执行,离开马路就是线程执行完成,看见红灯就是表示线程阻塞,不能执行。

    这个举例是不是很形象呢? 反正我就是这样记住信号量的语义的。

    下面我们首先介绍信号量模型,之后介绍如何使用信号量以及使用场景,最后我们再用信号量来实现一个限流器。

    同时如果看到这里你依然感兴趣,那么请你带着这样一个问题往下看。

    通过上面的讲解你觉得 Semaphore 和 Lock 有什么区别呢?既然有 Java SDK 里面提供了 Lock,为啥还要提供一个 Semaphore ?

    如果你不能回答这个问题,那么往下看,看完你要是还不知道,我就不用说啥了。

    信号量模型

    信号量模型还是很简单的,可以简单概括为:一个计数器,一个等待队列,三个方法。在信号量模型里,计数器和等待队列对外是透明的,所以只能通过信号量模型提供的三个方法来访问它们,这三个方法分别是:init()、down() 和 up()。你可以结合下图来形象化地理解。

    在Java SDK 中,信号量模型的实现就是由我们的 java.util.concurrent.Semaphore 实现的,而上面的三个方法都是原子的, Semaphore 类能够保证这三个方法的原子操作。

    • init() 方法,即初始化计数器的值,对应的就是Semaphore的构造器方法。
    • down() :计算器的值减一,如果此时计数器的值「小于0」,则当前线程被阻塞,也就是上面例子中允许同行的车辆已满,否则可以继续执行。
    • up()方法:计数器的值加1;如果此时计数器的值「小于或者等于0」,则唤醒队列中的一个线程,并将其从阻塞队列中移除。

    上面的方法如果你觉得不好理解,那么可以看下 JDK 中的具体实现 Semaphore 中提供的方法。

    信号量应用场景

    • Semaphore可以用于做流量控制,特别是公用资源有限的应用场景,比如数据库连接。假如有一个需求,要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个线程并发地读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这时我们必须控制只有10个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。这个时候,就可以使用Semaphore来做流量控制。

    • 用于限制接口调用大并发量,保护系统可用性,这也是一开始我说的我们之前的用法;

    • Semaphore 也是可以用来实现互斥锁的;原理很简单,就是在进入进阶区代码前我们使用 acquire() 操作,执行完退出临界区代码之前执行一下 release() 操作。特殊的点就再于,我们初始化时生命计数器为1即可。

    如果上面的代码没看懂,那么你就要问下自己应该不适合做程序员。

    这里是条分割线 。。

    解释下上面代码的执行流程,信号量是如何保证互斥的。

    假设两个线程 T1 和 T2 同时访问 addOne() 方法,当它们同时调用 acquire() 的时候,由于 acquire() 是一个原子操作,所以只能有一个线程(假设 T1)把信号量里的计数器减为 0,另外一个线程(T2)则是将计数器减为 -1。对于线程 T1,信号量里面的计数器的值是 0,大于等于 0,所以线程 T1 会继续执行;对于线程 T2,信号量里面的计数器的值是 -1,小于 0,按照信号量模型里对 down() 操作的描述,线程 T2 将被阻塞。所以此时只有线程 T1 会进入临界区执行count+=1;。

    当线程 T1 执行 release() 操作,也就是 up() 操作的时候,信号量里计数器的值是 -1,加 1 之后的值是 0,小于等于 0,按照信号量模型里对 up() 操作的描述,此时等待队列中的 T2 将会被唤醒。于是 T2 在 T1 执行完临界区代码之后才获得了进入临界区执行的机会,从而保证了互斥性。

    限流器的实现

    开头我说了,我们在工作中是用Semaphore来实现接口调用时的限流的,那么如何实现呢?

    在代码中,虽然有30个线程在执行,但是只允许10个并发执行。Semaphore的构造方法Semaphore(int permits)接受一个整型的数字,表示可用的许可证数量。Semaphore(10)表示允许10个线程获取许可证,也就是最大并发数是10。Semaphore的用法也很简单,首先线程使用Semaphore的acquire()方法获取一个许可证,使用完之后调用release()方法归还许可证。

    简言之,使用信号量,我们可以轻松地实现一个限流器,使用起来还是非常简单的。

    接下来要做的就是去寻找工作中合适的业务场景接入了,掌握了这个面试Java并发你也能扯一扯了。

    看完文章的你在 Java并发编程 这块又掌握了一个常用的知识点,恭喜你。

    前面问了一个问题,「Semaphore和Lock之间的区别是什么呢?」

    这里总结下:

    • 共同点:Semaphore也可以实现互斥锁的功能;
    • 区别:Semaphore 还有一个功能是 Lock 不容易实现的,那就是:「Semaphore 可以允许多个线程访问一个临界区。」

    原创不易,希望你能帮我个小忙呗,如果本文内容你觉得有所收获,请帮忙点个“在看”,或者转发分享让更多的小伙伴看到。


     Java并发|Atomic包下的原子操作类

     我是如何从电脑小白走上编程之路

     volatile关键字你不了解?赶紧来看看


    相关文章

      网友评论

        本文标题:Java并发|Semaphore如何实现限流器

        本文链接:https://www.haomeiwen.com/subject/qjrgmktx.html