并发编程之 Semaphore 源码分析
前言
并发 JUC 包提供了很多工具类,比如之前说的 CountDownLatch,CyclicBarrier ,今天说说这个 Semaphore——信号量,关于他的使用请查看往期文章并发编程之 线程协作工具类,今天的任务就是从源码层面分析一下他的原理。
源码分析
如果先不看源码,根据以往我们看过的 CountDownLatch CyclicBarrier 的源码经验来看,Semaphore 会怎么设计呢?
首先,他要实现多个线程线程同时访问一个资源,类似于共享锁,并且,要控制进入资源的线程的数量。
如果根据 JDK 现有的资源,我们是否可以使用 AQS 的 state 变量来控制呢?类似 CountDownLatch 一样,有几个线程我们就为这个 state 变量设置为几,当 state 达到了阈值,其他线程就不能获取锁了,就需要等待。当 Semaphore 调用 release 方法的时候,就释放锁,将 state 减一,并唤醒 AQS 上的线程。
以上,就是我们的猜想,那我们看看 JDK 是不是和我们想的一样。
首先看看 Semaphore 的 UML 结构:

内部有 3 个类,继承了 AQS。一个公平锁,一个非公平锁,这点和 ReentrantLock 一摸一样。
看看他的构造器:
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
两个构造器,两个参数,一个是许可线程数量,一个是是否公平锁,默认非公平。
而 Semaphore 有 2 个重要的方法,也是我们经常使用的 2 个方法:
semaphore.acquire();
// doSomeing.....
semaphore.release();
acquire 和 release 方法,我们今天重点看这两个方法的源码,一窥 Semaphore 的全貌。
acquire 方法源码分析
代码如下:
public void acquire() throws InterruptedException {
// 尝试获取一个锁
sync.acquireSharedInterruptibly(1);
}
// 这是抽象类 AQS 的方法
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 如果小于0,就获取锁失败了。加入到AQS 等待队列中。
// 如果大于0,就直接执行下面的逻辑了。不用进行阻塞等待。
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
// 这是抽象父类 Sync 的方法,默认是非公平的
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
// 非公平锁的释放锁的方法
final int nonfairTryAcquireShared(int acquires) {
// 死循环
for (;;) {
// 获取锁的状态
int available = getState();
int remaining = available - acquires;
// state 变量是否还足够当前获取的
// 如果小于 0,获取锁就失败了。
// 如果大于 0,就循环尝试使用 CAS 将 state 变量更新成减去输入参数之后的。
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
这里的释放就是对 state 变量减一(或者更多)的。
返回了剩余的 state 大小。
当返回值小于 0 的时候,说明获取锁失败了,那么就需要进入 AQS 的等待队列了。代码入下:
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 添加一个节点 AQS 队列尾部
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
// 死循环
for (;;) {
// 找到新节点的上一个节点
final Node p = node.predecessor();
// 如果这个节点是 head,就尝试获取锁
if (p == head) {
// 继续尝试获取锁,这个方法是子类实现的
int r = tryAcquireShared(arg);
// 如果大于0,说明拿到锁了。
if (r >= 0) {
// 将 node 设置为 head 节点
// 如果大于0,就说明还有机会获取锁,那就唤醒后面的线程,称之为传播
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
// 如果他的上一个节点不是 head,就不能获取锁
// 对节点进行检查和更新状态,如果线程应该阻塞,返回 true。
if (shouldParkAfterFailedAcquire(p, node) &&
// 阻塞 park,并返回是否中断,中断则抛出异常
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
// 取消节点
cancelAcquire(node);
}
}
总的逻辑就是:
创建一个分享类型的 node 节点包装当前线程追加到 AQS 队列的尾部。
如果这个节点的上一个节点是 head ,就是尝试获取锁,获取锁的方法就是子类重写的方法。如果获取成功了,就将刚刚的那个节点设置成 head。
如果没抢到锁,就阻塞等待。
release 方法源码分析
该方法用于释放锁,代码如下:
public void release() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
// 死循环释放成功
if (tryReleaseShared(arg)) {
// 唤醒 AQS 等待对列中的节点,从 head 开始
doReleaseShared();
return true;
}
return false;
}
// Sync extends AbstractQueuedSynchronizer
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
// 对 state 变量 + 1
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
这里释放锁的逻辑写在了抽象类 Sync 中。逻辑简单,就是对 state 变量做加法。
在加法成功后,执行 doReleaseShared 方法,这个方法是 AQS 的。
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
// 设置 head 的等待状态为 0 ,并唤醒 head 上的线程
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
// 成功设置成 0 之后,将 head 状态设置成传播状态
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
该方法的主要作用就是从 AQS 的 head 节点开始唤醒线程,注意,这里唤醒是 head 节点的下一个节点,需要和 doAcquireSharedInterruptibly 方法对应,因为 doAcquireSharedInterruptibly 方法唤醒的当前节点的上一个节点,也就是 head 节点。
至此,释放 state 变量,唤醒 AQS 头节点结束。
总结
总结一下 Semaphore 的原理吧。
总的来说,Semaphore 就是一个共享锁,通过设置 state 变量来实现对这个变量的共享。当调用 acquire 方法的时候,state 变量就减去一,当调用 release 方法的时候,state 变量就加一。当 state 变量为 0 的时候,别的线程就不能进入代码块了,就会在 AQS 中阻塞等待。
并发编程之 Semaphore 源码分析的更多相关文章
- 并发编程之 Exchanger 源码分析
前言 JUC 包中除了 CountDownLatch, CyclicBarrier, Semaphore, 还有一个重要的工具,只不过相对而言使用的不多,什么呢? Exchange -- 交换器.用于 ...
- 并发编程之 Condition 源码分析
前言 Condition 是 Lock 的伴侣,至于如何使用,我们之前也写了一些文章来说,例如 使用 ReentrantLock 和 Condition 实现一个阻塞队列,并发编程之 Java 三把锁 ...
- 并发编程之 CyclicBarrier 源码分析
前言 在之前的介绍 CountDownLatch 的文章中,CountDown 可以实现多个线程协调,在所有指定线程完成后,主线程才执行任务. 但是,CountDownLatch 有个缺陷,这点 JD ...
- Java并发编程之ThreadLocal源码分析
## 1 一句话概括ThreadLocal<font face="微软雅黑" size=4> 什么是ThreadLocal?顾名思义:线程本地变量,它为每个使用该对象 ...
- Java并发编程之ReentrantLock源码分析
ReentrantLock介绍 从JDK1.5之前,我们都是使用synchronized关键字来对代码块加锁,在JDK1.5引入了ReentrantLock锁.synchronized关键字性能比Re ...
- Java并发编程之AbstractQueuedSynchronizer源码分析
为什么要说AbstractQueuedSynchronizer呢? 因为AbstractQueuedSynchronizer是JUC并发包中锁的底层支持,AbstractQueuedSynchroni ...
- 并发编程之 CountDown 源码分析
前言 Doug Lea 大神在 JUC 包中为我们准备了大量的多线程工具,其中包括 CountDownLatch ,名为倒计时门栓,好像不太好理解.不过,今天的文章之后,我们就彻底理解了. 如何使用? ...
- 并发编程之ThreadLocal源码分析
当访问共享的可变数据时,通常需要使用同步.一种避免同步的方式就是不共享数据,仅在单线程内部访问数据,就不需要同步.该技术称之为线程封闭. 当数据封装到线程内部,即使该数据不是线程安全的,也会实现自动线 ...
- 异步编程之co——源码分析
异步编程系列教程: (翻译)异步编程之Promise(1)--初见魅力 异步编程之Promise(2):探究原理 异步编程之Promise(3):拓展进阶 异步编程之Generator(1)--领略魅 ...
随机推荐
- spring中的两个数据库事务DataSourceTransactionManager 和 JtaTransactionManager区别
在spring与数据框架(mybatis或hibernate)容器的配置文件中通常会有 <!-- (事务管理)transaction manager, use JtaTransactionMan ...
- Android常用库和插件
下拉刷新 PullLoadMoreRecyclerView 实现RecyclerView下拉刷新和上拉加载更多以及RecyclerView线性.网格.瀑布流效果演示 https://github.co ...
- C# webservice服务跟踪调试方法(转)
1.新建网站,添加服务,并创建服务. 2.打开internet 信息服务管理器,添加网站,映射到创建的服务所在网站的目录. 3.打开服务所在网站的解决方案,进行配置. 1) 设置启动选项 选择启动操作 ...
- cefsharp
快速上手 js和C#互相调用. C#调用js比较容易.JS调用C#代码,现有两种方法.老方法的缺点是只支持单页,如果切换页面,原有创建的变量就失效了.新方法没有这些问题. 老方法: Cefsharp ...
- WPF 打印界面(控件)到A4纸
这次遇到一个需求,就是将整个界面打印在A4纸上. 需求清楚后,Bing一下关于打印,就找到一个类PrintDialog ,其中两个方法可能会用到: 特别是public void PrintVisual ...
- 剑指offer编程题Java实现——面试题4替换空格
题目描述 请实现一个函数,将一个字符串中的空格替换成“%20”.例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy. package Solution; ...
- 通过Metasploit生成各种后门
生成windows后门 1.首先生成后门 [root@localhost ~]# msfvenom -p windows/meterpreter/reverse_tcp -e x86/shikata_ ...
- [JavaScript] iframe更改了src后,父页面history.back只能后退iframe而不能使自己后退解决办法
浏览器的机制如此,在iframe导航变化后手动点击浏览器的后退按钮也依然只是后退iframe中的导航. 有一种解决方案是不要修改iframe.src,而是删除旧iframe元素,新建一个iframe元 ...
- Linux巩固记录(6) Hbase环境准备-zookeeper安装
Hbase是运行在hadoop之上,所以请参考第3篇文章搭建好一个master,两个slave的hadoop环境,我采用的版本为hadoop2.7.4 不了解Hbase的同学可以参考下这篇文章,分析得 ...
- iOS-button利用block封装按钮事件【runtime 关联】
用block封装最常用的就是网络请求的回调,其实也可以结合category封装button的按钮事件,同时利用runtime的对象关联: UIButton+wkjButton.h 文件 #import ...