大家好,我是程序员牛肉。
对于 Java 初学者而言,JUC是突破高并发编程的关键跳板。其中精妙的设计思想 —— 从锁优化到无锁并发,从线程协同到资源调度 —— 堪称并发编程的 "设计模式宝典"。
我正在挑战讲完 JUC 的所有源码。点击关注,带你穿透 API 文档,从此不畏惧JUC拷打
今天我们来和大家一起读一下Semaphore。
大家经常把Semaphore叫做“信号量”,其用途我们可以直接看Doug lea留下来的注释:
[信号量(Semaphore)在概念上维护一组许可(permits)。每次调用 acquire()
方法时,若有必要会阻塞线程直到获得许可,然后占用该许可。每次调用 release()
方法会增加一个许可,可能唤醒阻塞的获取者。需要注意的是,Semaphore实际并无物理许可对象;Semaphore
仅通过计数器跟踪可用许可数量并据此操作]
那其实这里就有一个问题了,Semaphore看起来和Lock没什么区别。都是基于许可来确定是否要阻塞当前线程。那Doug lea为什么要创建出来两个逻辑差不多的类?
因为从设计思想上来讲,Lock是为了实现严格实现互斥访问(Mutual Exclusion),确保同一时刻仅有一个线程能访问共享资源(如临界区代码或数据)。
因此当Lock在释放凭证(permits)的时候,他会判断一下当前的线程是不是持有锁的线程:
而Semaphore的本质只是释放出一批通行证,只要你获取了就可以通行,因此Semaphore在释放凭证的时候,并不关注当前的线程是不是持有锁的线程:
因此如果我们做一个形象的比喻的话,我们可以将Semaphore看做是停车场中显示剩余车位的告知牌。
Semaphore并不管这是谁的车,Semaphore只关注当前的车位还够不够你停进去。
让我们从源码角度来认识一下Semaphore吧。Semaphore有一个继承于AQS的sync内部类,几乎所有的锁在实现的时候都用到了sync内部类。
Semaphore和其他锁一样支持公平锁和非公平锁。
而之所以能实现公平锁,是因为其依赖了AQS框架。当线程想要获取凭证的时候就要先进队列中进行排队,严格执行先到先得。
在这里面调用hasQueuedPredcessors方法判断了一下队列中是否还有比自己靠前的元素:
public final boolean hasQueuedPredecessors() {
// h = 头节点, s = 待检查的候选节点
Node h, s;
// 1. 检查队列是否非空(head != null)
if ((h = head) != null) {
// 2. 检查头节点的直接后继节点(第一个等待节点)
if ((s = h.next) == null || s.waitStatus > ) {
// 情况:后继节点不存在或已取消(waitStatus > 0 表示 CANCELLED)
s = null; // 初始化s
// 3. 从尾节点向前遍历查找有效节点
for (Node p = tail; p != h && p != null; p = p.prev) {
if (p.waitStatus <= ) // 跳过已取消的节点
s = p; // 记录最后一个有效节点
}
}
// 4. 判断找到的有效节点是否属于当前线程
if (s != null && s.thread != Thread.currentThread())
returntrue; // 存在其他排队线程
}
// 5. 默认返回false(无线程排队或当前线程是第一个)
returnfalse;
}
而非公平锁的设计就很简单了,只需要写死一个for循环之后不断的尝试获取凭证就好了:
剩下的方法也没有什么好讲的了。当你回头看去JUC的大部分锁的时候,其实你就会发现这些锁其实内部没什么方法,大部分核心逻辑都交给AQS以及sync包去做了。
这种“控制反转”和“模板方法”的设计模式,在 JUC 包中体现得淋漓尽致,也是我们学习源码时最值得品味的地方。
那今天关于Semaphore的源码讲解就到这里了。相信通过我的介绍,你已经了解了Semaphore的设计思想。
对于Semaphore你还有什么想聊的吗?欢迎在评论区留言。