Java 并发编程实战
王宝令
资深架构师
72485 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 51 讲
学习攻略 (1讲)
Java 并发编程实战
15
15
1.0x
00:00/00:00
登录|注册

16 | Semaphore:如何快速实现一个限流器?

对象保存在Vector和ArrayList的比较
管程模型
信号量在Java语言里的地位
对象池示例代码
限流器
互斥锁功能
up()
down()
init()
课后思考
总结
快速实现一个限流器
如何使用信号量
信号量模型
Semaphore

该思维导图由 AI 生成,仅供参考

Semaphore,现在普遍翻译为“信号量”,以前也曾被翻译成“信号灯”,因为类似现实生活里的红绿灯,车辆能不能通行,要看是不是绿灯。同样,在编程世界里,线程能不能执行,也要看信号量是不是允许。
信号量是由大名鼎鼎的计算机科学家迪杰斯特拉(Dijkstra)于 1965 年提出,在这之后的 15 年,信号量一直都是并发编程领域的终结者,直到 1980 年管程被提出来,我们才有了第二选择。目前几乎所有支持并发编程的语言都支持信号量机制,所以学好信号量还是很有必要的。
下面我们首先介绍信号量模型,之后介绍如何使用信号量,最后我们再用信号量来实现一个限流器。

信号量模型

信号量模型还是很简单的,可以简单概括为:一个计数器,一个等待队列,三个方法在信号量模型里,计数器和等待队列对外是透明的,所以只能通过信号量模型提供的三个方法来访问它们,这三个方法分别是:init()、down() 和 up()。你可以结合下图来形象化地理解。
信号量模型图
这三个方法详细的语义具体如下所示。
init():设置计数器的初始值。
down():计数器的值减 1;如果此时计数器的值小于 0,则当前线程将被阻塞,否则当前线程可以继续执行。
up():计数器的值加 1;如果此时计数器的值小于或者等于 0,则唤醒等待队列中的一个线程,并将其从等待队列中移除。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

信号量在并发编程中扮演着重要角色,本文介绍了它的基本概念和使用方法。通过信号量,可以实现限流器的功能,文章详细讲解了信号量模型、互斥锁的实现以及限流器的快速实现。此外,还提到了信号量在Java语言中的应用和与管程模型的比较。总的来说,本文全面介绍了信号量在并发编程中的重要性和实际应用。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Java 并发编程实战》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(128)

  • 最新
  • 精选
  • CCC
    我理解的和管程相比,信号量可以实现的独特功能就是同时允许多个线程进入临界区,但是信号量不能做的就是同时唤醒多个线程去争抢锁,只能唤醒一个阻塞中的线程,而且信号量模型是没有Condition的概念的,即阻塞线程被醒了直接就运行了而不会去检查此时临界条件是否已经不满足了,基于此考虑信号量模型才会设计出只能让一个线程被唤醒,否则就会出现因为缺少Condition检查而带来的线程安全问题。正因为缺失了Condition,所以用信号量来实现阻塞队列就很麻烦,因为要自己实现类似Condition的逻辑。

    作者回复: 👍👍👍

    2019-04-04
    8
    268
  • 老杨同志
    需要用线程安全的vector,因为信号量支持多个线程进入临界区,执行list的add和remove方法时可能是多线程并发执行

    作者回复: 👍

    2019-04-04
    7
    170
  • 任大鹏
    有同学认为up()中的判断条件应该>=0,我觉得有可能理解为生产者-消费者模式中的生产者了。可以这么想,>0就意味着没有阻塞的线程了,所以只有<=0的情况才需要唤醒一个等待的线程。其实down()和up()是成对出现的,并且是先调用down()获得锁,处理完成再调用up()释放锁,如果信号量初始值为1,应该是不会出现>0的情况的,除非故意调先用up(),这也失去了信号量本身的意义了。不知道我理解的对不对。

    作者回复: 对👍👍👍

    2019-04-04
    3
    54
  • Alvan
    很多人对up()方法的计数器count<=0不理解,可以看下这里: 1、反证法验证一下,假如一个线程先执行down()操作,那么此时count的值是0,接着这个线程执行up()操作,此时count的值是1,如果count应该是大于等于0,那么应该唤醒其他线程,可是此时并没有线程在睡眠呀,count的值不应该是大于等于0。 2、假如一个线程t1执行down()操作,此时count = 0,然后t1被中断,另外的线程t2执行down()操作,此时count=-1,t2阻塞睡眠,另外的线程t3执行down()操作,count=-2,t3也睡眠。count=-2 说明有两个线程在睡眠,接着t1执行up() 操作,此时count=-1,小于等于0,唤醒t2或者t3其中一个线程,假如计数器count是大于等于0才唤醒其他线程,这明显是不对的。

    作者回复: 👍

    2019-09-09
    5
    30
  • 木卫六
    换ArrayList是不行的,临界区内可能存在多个线程来执行remove操作,出现不可预知的后果。 对于chaos同学说return之前释放的问题,我觉得可以这么理解:return的是执行后的结果,而不是“执行”。所以顺序应该是这样的:1acquire;2apply;3finally release;4return2的结果

    作者回复: 是的,感谢回复的这么详细!!!

    2019-04-04
    4
    21
  • 缪文
    这个限流器实际上限的是并发量,也就是同时允许多少个请求通过,如果限制每秒请求数,不是这个实现的吧

    作者回复: 后面会介绍guava的限流器

    2019-04-06
    15
  • 刘彦辉
    假如有3个线程,线程A、B、C,信号量计数器为1,线程A执行down的时候变为0,不阻塞;线程B执行down,变为-1,阻塞;线程C执行down变为-2,阻塞。当线程A执行完,调用up后,变为-1,此时唤醒一个线程,那么请问唤醒之后的操作呢?唤醒之后直接就执行了业务代码了?还是唤醒之后还需要去先执行down?按分析的话应该不能执行down了,如果执行down的话,计数器变为-2,还会阻塞,所以是不是这块儿的阻塞和唤醒也是用的wait和notify呢?唤醒之后,从阻塞的代码开始继续执行,这样就可以成功执行下去了。麻烦老师解答一下哈,谢谢。

    作者回复: 唤醒后直接执行业务代码,在哪里阻塞,唤醒后就在哪里继续执行

    2019-09-20
    3
    10
  • crazypokerk
    老师,那个计数器中得s.acquire()是需要捕获异常的。 static int count; static final Semaphore s = new Semaphore(1); static void addOne() throws InterruptedException { s.acquire(); try { count += 1; }finally { s.release(); } }

    作者回复: 异常都被我省略了,这样代码更能专注的表达问题,如果你本地实验,加上就可以了。手机屏幕太小,折行后行数太多,看到后面忘了前面,所以我尽讲精炼代码

    2019-04-04
    4
    9
  • ken
    public class Food { public String name; private long warmTime; public Food(String name, long warmTime) { this.name = name; this.warmTime = warmTime; } public String getName() { return name; } public long getWarmTime() { return warmTime; } } public class MicrowaveOven { public String name; public MicrowaveOven(String name) { this.name = name; } public Food warm(Food food) { long second = food.getWarmTime() * 1000; try { Thread.sleep(second); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(String.format("%s warm %s %d seconds food.", name,food.getName() ,food.getWarmTime())); return food; } public String getName() { return name; } } public class MicrowaveOvenPool { private List<MicrowaveOven> microwaveOvens; private Semaphore semaphore; public MicrowaveOvenPool(int size,@NotNull List<MicrowaveOven> microwaveOvens) { this.microwaveOvens = new Vector<>(microwaveOvens); this.semaphore = new Semaphore(size); } public Food exec(Function<MicrowaveOven, Food> func) { MicrowaveOven microwaveOven = null; try{ semaphore.acquire(); microwaveOven = microwaveOvens.remove(0); return func.apply(microwaveOven); }catch (InterruptedException e) { e.printStackTrace(); } finally { microwaveOvens.add(microwaveOven); semaphore.release(); } return null; } }

    作者回复: 👍

    2019-04-08
    2
    7
  • 长眉_张永
    对于进入的多个线程资源之间,如果有公用的信息的话,是否还需要加锁操作呢?

    作者回复: 需要

    2019-04-09
    2
    6
收起评论
显示
设置
留言
99+
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部