写点什么

《重学 Java 高并发》Sempahore 的使用场景与常见误区

  • 2021 年 11 月 28 日
  • 本文字数:2112 字

    阅读完需:约 7 分钟

《重学Java高并发》Sempahore的使用场景与常见误区

大家好,我是威哥,《RocketMQ 技术内幕》一书作者,荣获 RocketMQ 官方社区优秀布道师、CSDN2020 博客执之星 Top2 等荣誉称号。目前担任中通快递技术平台部资深架构师,主要负责全链路压测、消息中间件、数据同步等产品的研发与落地,拥有千亿级消息集群的运维经验,不仅实践经验丰富,而且对其源代码有深入且系统的研究。欢迎大家关注我,一起抱团发展。


JUC,java 并发框架也是面试中的常客,而 Semaphore 信号量又是 JUC 的重头戏。


Semaphore 简单吗?使用起来非常简单,但最近生产环境遇到一个故障,最终原因竟然是 Semaphore 在多线程异步环境使用不当造成的?


读者朋友们一定会觉得很诧异,容我慢慢道来。


文章开头时先吹一个牛:我有一双“火眼金睛”,任你刷多少题,是否用过 Semaphore,我一问便知。

1、Sempahore 使用场景

读者朋友们对下面的对话我想肯定不会陌生:


面试官:看你简历中写到你熟悉多线程编程,那你的多线程工具包有哪些工具?


候选人:多线程 jdk 提供了丰富的工具,都集中在 JUC 包中,通常有线程池、Semaphore、CountDownLatch、原子类等。面试官:那你能说说信号量 Semaphore 通常在什么场景下使用呢?候选人:限流面试官:如何基于 Semaphore 进行限流?候选人:。。。。。。面试官:看图说话,这段代码是否正确?



Sempahore 信号量最经典的使用场景是限流,用于控制并发度。


回想我们在开发系统时免不了要对接第三方系统,例如在快递行业的用户端系统(寄件)时,用户通过微信小程序进行下单时,需要手动填写收件人、寄件人信息等信息,十分繁琐与低效,能否从产品角度加以改善?


当然可以,产品提成上传一张包含收件地址等信息等图片,通过 AI 等技术识别图片,自动提取图片中的有效信息并自动填充,提高用户体验。


通过技术选型,我们敲定了百度的免费(付费)图片识别接口,但第三方提供的配额是有限的,特别是会限制接口的并发度,超过并发度的接口将会返回错误,特别是免费类的接口更加如此?


该如何处理呢?Sempahore 闪亮登场

2、许可超额现象及解决方案

Sempahore 的使用场景非常经典,其使用看上区非常简单,其基本的使用代码如下:



上面的方法就是控制 doSomeThing()方法的调用并发度,即同一时间允许多少个线程并发执行 doSomeThing()方法中的代码,简单说明一下:


  • 在创建 Sempahore 对象时指定该信号量中包含的许可数量。

  • 在执行业务方法之前,首先向信号量对象中申请一个许可,如果申请到,acquire()方法立即返回,执行完成业务逻辑放一定要调用 release()方法,释放许可。

  • 如果当前许可已全部申请,其他线程调用信号量的 acquire()方法时会阻塞,当然 acquire 方法也可以设置超时时间,该方法的返回结果表示是否申请到许可。


温馨提示:上面的代码有漏洞,你能发现吗?


上面的代码有可能造成多释放,当 10 个线程分别去获取许可,都能成功,但都在执行 doSomeThing()方法的时候,第 11 个线程尝试获取许可,但可能发生中断等异常导致没成功获取许可而触发异常,但最终进入到 finally 语句块,进行释放许可,这样就会增加许可数量,导致逻辑异常,这样的问题特别是在调用带超时时间的 acquire 方法时更加明显,其正确的使用如下图所示:


3、许可不释放导致无限阻塞

上面只是“小试牛刀”,使用 Sempahore 真正的挑战在于多线程环境中,我们回到开头部分的场景,调用第三方接口解析图片,并使用多线程提高并发度,示例代码如下:



该示例主要是结合 CompletableFuture 实现多线程并发,并结合信号量控制调用第三方接口的并发度(这里用 doSomeThing()方法表示)。


温馨提示:CompleteableFuture 的 whenComplete 事件回调函数是发生异常时也会进入,并且第二个参数为异常对象。


在 JDK8 中 CompletableFuture 暂不支持设置超时时间,故本例使用了 CountDownLatch 用来控制 test02 方法的超时时间。


上面的示例是不是非常给力,但如果细看,可能存在许可不及时释放的问题,也就示说 semaphore 的 release()方法不会执行,因为上述方法会超时。


许可不释放带来的后果非常严重,因为后续申请的时候由于一直没有许可,将无法获取许可,而无法执行业务逻辑。


初步解决思路就是在 cdh.awiat 方法结束后判断是否超时,如果超时,手动释放许可,例如下图所示:



这样的想法好是好,但这样会造成许可的多次释放,最终导致许可数量增加,超过预期。


这其实是要求 seaphore 的 release 方法会在不同条件下在不同地方会被调用,但同一个请求只在其中一个地方被执行。


要解决这个问题,我想大家会自然而然的想到 JUC 中的另外的工具:原子类,尽管多次调用,我们只需第一次调用时真正释放许可,其他调用则直接忽略即可。


解决方案如下:



引入一个包装类,包装 Sempahore,并结合 AtomicBoolean,保证每一个 SempahoreReleaseOnlyOne 对象只会释放 Sempahore 一次。


引入该类后,上面的代码改造如下:



本文就介绍到这里了,Semaphore 看似简单,但要用好用对还是有难度的,不知各位是否 Get 到了。如果需要整套源码,可以发送私信:SCODE,即可获取。


文章首发于https://www.codingw.net/posts/fa8c5e0b.html


作者简介:丁威,《RocketMQ 技术内幕》一书作者、RocketMQ 开源社区优秀布道师,公众号「中间件兴趣圈」维护者,主打成体系剖析 Java 主流中间件,已发布 Kafka、RocketMQ、Dubbo、Sentinel、Canal、ElasticJob 等中间件 15 个专栏。

发布于: 2021 年 11 月 28 日阅读数: 10
用户头像

『中间件兴趣圈』《RocketMQ技术内幕》 2020.11.30 加入

《RocketMQ技术内幕》作者、RocketMQ社区优秀布道师、中通科技技术平台部资深架构师、专注于JAVA中间件领域的源码分析、原理与实战。

评论

发布
暂无评论
《重学Java高并发》Sempahore的使用场景与常见误区