Semaphore、CountDownLatch 的实现原理浅析

2,063 阅读3分钟

前言

搞懂这篇文章的前提是对 Lock 的几种实现以及 AQS 的源码原理有一定了解,如果不了解的话可以看下 Lock 中的 AQS、独占锁、重入锁、读锁、写锁、Condition 源码原理分析,本文源码未贴出来自己去翻下然后跟着图的调用逻辑走就能理清了

Semaphore

Semaphore(信号量)它通过 new Semaphore(permits) 来进行创建,permits 表示同一时间可以执行多少个线程。

使用 acquire 来获得许可,通过 release 来释放许可。在同一时间只允许 permits 个线程同时运行。

可以看到输出结果,当线程数量达到上限的时候,其它线程无法执行,释放了一个线程后归还一个信号量,那么下一个线程才能执行。

它的实现原理如下

  1. 当调用 new Semaphore(5); 的时候就是说同一时刻只能有 5 个线程在同时执行。它的内部实现是创建了一个 NonfairSync,5 表示 state 的初始值,也就是信号量
  2. 申请许可的时候,其实就是在调用 acquire()
  3. 获取共享锁就是调用 NonfairSync 的 nonfairTryAcquireShared 方法
  4. 获取一个信号量的话 tmp = state - 1,如果 tmp < 0 了表明"过载"了申请许可失败会将当前线程构造成 Waiter 结点放入同步队列中
  5. 如果 tmp >= 0 的话,则调用 CAS 去更新值,在这个过程由于存在并发的情况失败了会无限循环调用 CAS 设置
  6. 如果 CAS 设置成功,那么持有锁成功
  7. 如果 CAS 设置锁失败(会不停的循环设置直到发现 state < 0 了还没有成功,如下图)会将当前线程构造成 Waiter 结点放入同步队列中
  8. 同步队列中的线程处于等待状态,当调用 release 后或者发出线程中断操作后,这个线程又会开始从头结点开始争抢锁

CountDownLatch

CountDownLatch 内部维护了一个计数器,当计数器不为 0 的时候调用其 await() 可以进行阻塞,每次使用 countDown() 计数器值 - 1,当计数器值为 0 的时候,所有阻塞的线程从 await() 返回

利用这个特性我们可以用来合并多个线程最终的结果,或者以此来模拟并发请求调用等等,如下并发请求代码

此处的“锁”的成功与否表现其实就是 CountDownLatch 的一个同步值的变化

我们来分析下 CountDownLatch 是如何做到的以下两张图分别展示了在 countDown() 和 await() 的原理,源码跟着看就行了就不贴出来了

  1. countDown 的时候每次调用都会对 state 减 1 也就是我们 new CountDownLatch(10); 的这个计数器的数字减 1,如果为 0 的话说明该唤醒同步队列中的等待结点了,如果大于 0 的话这直接返回。

此时唤醒同步队列中的等待结点后,await() 中挂起的线程将被唤醒,然后再次将下一个结点设置为头结点,唤醒下一个结点如此往复,最终所有的线程将被唤醒

  1. 此时后面调用了 await 方法它会去获取同步值发现为 0 的话成功返回,如果小于 0 的话,再次判断是否是头结点,如果是的话再次尝试获取同步值,如果发现 state 为 0 的话则将当前节点断开,设置同步队列中的下一个结点为头结点,调用释放锁逻辑。如果 state 不为 0 或者发现不是头结点就直接将线程挂起