面经手册 · 第18篇《AQS 共享锁,Semaphore、CountDownLatch,听说数据库连接池可以用到!》
作者:小傅哥
博客:https://bugstack.cn
Github:https://github.com/fuzhengwei/CodeGuide/wiki
沉淀、分享、成长,让自己和他人都能有所收获!
一、前言
学Java怎么能,突飞猛进的成长?
是不是你看见过的突飞猛进都是别人,但自己却很难!
其实并没有一天的突飞猛进,也没有一口吃出来的胖子。有得更多的时候日积月累、不断沉淀,最后才能爆发、破局!
举个简单的例子,如果你大学毕业时候已经写了40万行代码,还找不到工作吗?但40万行平均到每天并不会很多,重要的是持之以恒的坚持。
二、面试题
谢飞机,小记!
东风吹、战鼓擂,不加班、谁怕谁!哈哈哈,找我大哥去。
谢飞机:喂,大哥。我女友面试卡住了,强人锁
难,锁我也不会!
面试官:你不应该不会呀,问你一个,基于 AQS 实现的锁都有哪些?
谢飞机:嗯,有 ReentrantLock...
面试官:还有呢?
谢飞机:好像想不起来了,sync也不是!
面试官:哎,学点漏点,不思考、不总结、不记录。你这样人家面试你就没法聊了,最起码你要有点深度。
谢飞机:嘿嘿,记住了。来我家吃火锅吧,细聊。
三、共享锁 和 AQS
1. 基于 AQS 实现的锁有哪些?
AQS(AbstractQueuedSynchronizer),是 Java 并发包中非常重要的一个类,大部分锁的实现也是基于 AQS 实现的,包括:
ReentrantLock
,可重入锁。这个是我们最开始介绍的锁,也是最常用的锁。通常会与 synchronized 做比较使用。ReentrantReadWriteLock
,读写锁。读锁是共享锁、写锁是独占锁。Semaphore
,信号量锁。主要用于控制流量,比如:数据库连接池给你分配10个链接,那么让你来一个连一个,连到10个还没有人释放,那你就等等。CountDownLatch
,闭锁。Latch 门闩的意思,比如:说四个人一个漂流艇,坐满了就推下水。
这一章节我们主要来介绍 Semaphore ,信号量锁的实现,其实也就是介绍一个关于共享锁的使用和源码分析。
2. Semaphore 共享锁使用
Semaphore semaphore = new Semaphore(2, false); // 构造函数入参,permits:信号量、fair:公平锁/非公平锁
for (int i = 0; i < 8; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "蹲坑");
Thread.sleep(1000L);
} catch (InterruptedException ignore) {
} finally {
semaphore.release();
}
}, "蹲坑编号:" + i).start();
}
这里我们模拟了一个在高速服务区,厕所排队蹲坑的场景。由于坑位有限,为了避免造成拥挤和踩踏,保安人员在门口拦着,感觉差不多,一次释放两个进去,一直到都释放。你也可以想成早上坐地铁上班,或者旺季去公园,都是一批一批的放行
测试结果
蹲坑编号:0蹲坑
蹲坑编号:1蹲坑
蹲坑编号:2蹲坑
蹲坑编号:3蹲坑
蹲坑编号:4蹲坑
蹲坑编号:5蹲坑
蹲坑编号:6蹲坑
蹲坑编号:7蹲坑
Process finished with exit code 0
- Semaphore 的构造函数可以传递是公平锁还是非公平锁,最终的测试结果也不同,可以自行尝试。
- 测试运行时,会先输出
0坑、1坑
,之后2坑、3坑
...,每次都是这样两个,两个的释放。这就是 Semaphore 信号量锁的作用。
3. Semaphore 源码分析
3.1 构造函数
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
permits:n. 许可证,特许证(尤指限期的)
默认情况下只需要传入 permits 许可证数量即可,也就是一次允许放行几个线程。构造函数会创建非公平锁。如果你需要使用 Semaphore 共享锁中的公平锁,那么可以传入第二个构造函数的参数 fair = false/true。true:FairSync,公平锁。在我们前面的章节已经介绍了公平锁相关内容和实现,以及CLH、MCS 《公平锁介绍》
初始许可证
数量
FairSync/NonfairSync(int permits) {
super(permits);
}
Sync(int permits) {
setState(permits);
}
protected final void setState(int newState) {
state = newState;
}
在构造函数初始化的时候,无论是公平锁还是非公平锁,都会设置 AQS 中 state 数量值。这个值也就是为了下文中可以获取的信号量扣减和增加的值。
3.2 acquire 获取信号量
方法 | 描述 |
---|---|
semaphore.acquire() |
一次获取一个信号量,响应中断 |
semaphore.acquire(2) |
一次获取n个信号量,响应中断(一次占2个坑) |
semaphore.acquireUninterruptibly() |
一次获取一个信号量,不响应中断 |
semaphore.acquireUninterruptibly(2) |
一次获取n个信号量,不响应中断 |
- 其实获取信号量的这四个方法,主要就是,一次获取几个和是否响应中断的组合。
semaphore.acquire()
,源码中实际调用的方法是,sync.acquireSharedInterruptibly(1)
。也就是相应中断,一次只占一个坑。semaphore.acquire(2)
,同理这个就是一次要占两个名额,也就是许可证。生活中的场景就是我给我朋友排的对,她来了,进来吧。
3.3 acquire 释放信号量
方法 | 描述 |
---|---|
semaphore.release() |
一次释放一个信号量 |
semaphore.release(2) |
一次获取n个信号量 |
有获取就得有释放,获取了几个信号量就要释放几个信号量。当然你可以尝试一下,获取信号量 semaphore.acquire(2) 两个,释放信号量 semaphore.release(1),看看运行效果
3.4 公平锁实现
信号量获取过程,一直到公平锁实现。semaphore.acquire
-> sync.acquireSharedInterruptibly(permits)
-> tryAcquireShared(arg)
semaphore.acquire(1);
public void acquire(int permits) throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireSharedInterruptibly(permits);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
FairSync.tryAcquireShared
protected int tryAcquireShared(int acquires) {
for (;;) {
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
hasQueuedPredecessors
,公平锁的主要实现逻辑都在于这个方法的使用。它的目的就是判断有线程排在自己前面没,以及把线程添加到队列中的逻辑实现。在前面我们介绍过CLH等实现,可以往前一章节阅读for (;;)
,是一个自旋的过程,通过 CAS 来设置 state 偏移量对应值。这样就可以避免多线程下竞争获取信号量冲突。getState()
,在构造函数中已经初始化 state 值,在这里获取信号量时就是使用 CAS 不断的扣减。- 另外需要注意,共享锁和独占锁在这里是有区别的,独占锁直接返回true/false,共享锁返回的是int值。
- 如果该值小于0,则当前线程获取共享锁失败。
- 如果该值大于0,则当前线程获取共享锁成功,并且接下来其他线程尝试获取共享锁的行为很可能成功。
- 如果该值等于0,则当前线程获取共享锁成功,但是接下来其他线程尝试获取共享锁的行为会失败。
3.5 非公平锁实现
NonfairSync.nonfairTryAcquireShared
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
- 有了公平锁的实现,非公平锁的理解就比较简单了,只是拿去了
if (hasQueuedPredecessors())
的判断操作。 - 其他的逻辑实现都和公平锁一致。
3.6 获取信号量失败,加入同步等待队列
在公平锁和非公平锁的实现中,我们已经看到正常获取信号量的逻辑。那么如果此时不能正常获取信号量呢?其实这部分线程就需要加入到同步队列。
doAcquireSharedInterruptibly
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- 首先
doAcquireSharedInterruptibly
方法来自 AQS 的内部方法,与我们在学习竞争锁时有部分知识点相同,但也有一些差异。比如:addWaiter(Node.SHARED)
,tryAcquireShared
,我们主要介绍下这内容。 Node.SHARED
,其实没有特殊含义,它只是一个标记作用,用于判断是否共享。final boolean isShared() { return nextWaiter == SHARED; }
tryAcquireShared
,主要是来自Semaphore
共享锁中公平锁和非公平锁的实现。用来获取同步状态。setHeadAndPropagate(node, r)
,如果r > 0,同步成功后则将当前线程结点设置为头结点,同时 helpGC,p.next = null,断链操作。shouldParkAfterFailedAcquire(p, node)
,调整同步队列中 node 结点的状态,并判断是否应该被挂起。这在我们之前关于锁的文章中已经介绍。parkAndCheckInterrupt()
,判断是否需要被中断,如果中断直接抛出异常,当前结点请求也就结束。cancelAcquire(node)
,取消该节点的线程请求。
4. CountDownLatch 共享锁使用
CountDownLatch 也是共享锁的一种类型,之所以在这里体现下,是因为它和 Semaphore 共享锁,既相似有不同。
CountDownLatch 更多体现的组团一波的思想,同样是控制人数,但是需要够一窝。比如:我们说过的4个人一起上皮划艇、两个人一起上跷跷板、2个人一起蹲坑我没见过,这样的方式就是门闩 CountDownLatch 锁的思想。
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(10);
ExecutorService exec = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
exec.execute(() -> {
try {
int millis = new Random().nextInt(10000);
System.out.println("等待游客上船,耗时:" + millis + "(millis)");
Thread.sleep(millis);
} catch (Exception ignore) {
} finally {
latch.countDown(); // 完事一个扣减一个名额
}
});
}
// 等待游客
latch.await();
System.out.println("船长急躁了,开船!");
// 关闭线程池
exec.shutdown();
}
- 这一个公园游船的场景案例,等待10个乘客上传,他们比较墨迹。
- 上一个扣减一个
latch.countDown()
- 等待游客都上船
latch.await()
- 最后船长开船!!
急躁了
测试结果
等待游客上船,耗时:6689(millis)
等待游客上船,耗时:2303(millis)
等待游客上船,耗时:8208(millis)
等待游客上船,耗时:435(millis)
等待游客上船,耗时:9489(millis)
等待游客上船,耗时:4937(millis)
等待游客上船,耗时:2771(millis)
等待游客上船,耗时:4823(millis)
等待游客上船,耗时:1989(millis)
等待游客上船,耗时:8506(millis)
船长急躁了,开船!
Process finished with exit code 0
- 在你实际的测试中会发现,
船长急躁了,开船!
,会需要等待一段时间。 - 这里体现的就是门闩的思想,组队、一波带走。
- CountDownLatch 的实现与 Semaphore 基本相同、细节略有差异,就不再做源码分析了。
四、总结
- 在有了 AQS、CLH、MCS,等相关锁的知识了解后,在学习其他知识点也相对容易。基本以上和前几章节关于锁的介绍,也是面试中容易问到的点。可能由于目前分布式开发较多,单机的多线程性能压榨一般较少,但是对这部分知识的了解非常重要
- 得益于Lee老爷子的操刀,并发包锁的设计真的非常优秀。每一处的实现都可以说是精益求精,所以在学习的时候可以把小傅哥的文章当作抛砖,之后继续深挖设计精髓,不断深入。
- 共享锁的使用可能平时并不多,但如果你需要设计一款类似数据库线程池的设计,那么这样的信号量锁的思想就非常重要了。所以在学习的时候也需要有技术迁移的能,不断把这些知识复用到实际的业务开发中。
五、系列推荐
- volatile 怎么实现的内存可见?没有 volatile 一定不可见吗?
- synchronized 解毒,剖析源码深度分析!
- ReentrantLock之公平锁讲解和实现
- ReentrantLock之AQS原理分析和实践使用
- 想去BAT大厂,应该怎么面?
面经手册 · 第18篇《AQS 共享锁,Semaphore、CountDownLatch,听说数据库连接池可以用到!》的更多相关文章
- ReentrantReadWriteLock 源码分析以及 AQS 共享锁 (二)
前言 上一篇讲解了 AQS 的独占锁部分(参看:ReentrantLock 源码分析以及 AQS (一)),这一篇将介绍 AQS 的共享锁,以及基于共享锁实现读写锁分离的 ReentrantReadW ...
- JS魔法堂:不完全国际化&本地化手册 之 实战篇
前言 最近加入到新项目组负责前端技术预研和选型,其中涉及到一个熟悉又陌生的需求--国际化&本地化.熟悉的是之前的项目也玩过,陌生的是之前的实现仅仅停留在"有"的阶段而已. ...
- Mysql高手系列 - 第18篇:mysql流程控制语句详解(高手进阶)
Mysql系列的目标是:通过这个系列从入门到全面掌握一个高级开发所需要的全部技能. 这是Mysql系列第18篇. 环境:mysql5.7.25,cmd命令中进行演示. 代码中被[]包含的表示可选,|符 ...
- 面经手册 · 第17篇《码农会锁,ReentrantLock之AQS原理分析和实践使用》
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 如果你相信你做什么都能成,你会自信的多! 千万不要总自我否定,尤其是职场的打工人.如 ...
- 面经手册 · 第16篇《码农会锁,ReentrantLock之公平锁讲解和实现》
作者:小傅哥 博客:https://bugstack.cn 专题:面经手册 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 Java学多少才能找到工作? 最近经常有小伙伴问我,以为我的经验来看 ...
- 面经手册 · 第2篇《数据结构,HashCode为什么使用31作为乘数?》
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 在面经手册的前两篇介绍了<面试官都问我啥>和<认知自己的技术栈盲区 ...
- AQS共享锁应用之Semaphore原理
我们调用Semaphore方法时,其实是在间接调用其内部类或AQS方法执行的.Semaphore类结构与ReetrantLock类相似,内部类Sync继承自AQS,然后其子类FairSync和NoFa ...
- 小刻也能看懂的Unraid系统使用手册:基础篇
小刻也能看懂的Unraid系统使用手册 基础篇 Unraid系统简介 Unraid 的本体其实是 Linux,它主要安装在 NAS 和 All in One 服务器上,经常可以在 Linus 的视频里 ...
- JS魔法堂:不完全国际化&本地化手册 之 拓展篇
前言 最近加入到新项目组负责前端技术预研和选型,其中涉及到一个熟悉又陌生的需求--国际化&本地化.熟悉的是之前的项目也玩过,陌生的是之前的实现仅仅停留在"有"的阶段而已. ...
随机推荐
- Python3.7有什么新变化
https://docs.python.org/zh-cn/3/whatsnew/3.7.html
- 【数论】HAOI2012 容易题
题目大意 洛谷链接 有一个数列A已知对于所有的\(A[i]\)都是\(1~n\)的自然数,并且知道对于一些\(A[i]\)不能取哪些值,我们定义一个数列的积为该数列所有元素的乘积,要求你求出所有可能的 ...
- centos8使用systemd/systemctl管理系统/服务
一,systemd的用途? Systemd 是 Linux 系统工具,用来启动守护进程,已成为大多数发行版的标准配置 Systemd 的优点是功能强大,使用方便, 缺点是体系庞大,非常复杂 在cent ...
- 第十九章 keepalived高可用
一.keepalived高可用 1.什么是高可用 一般是指2台机器启动着完全相同的业务系统,当有一台机器down机了,另外一台服务器就能快速的接管,对于访问的用户是无感知的. 2.高可用使用的工具 1 ...
- django—csrf中间件校验流程
CSRF(跨站请求伪造)是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法. 这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求 ...
- 常用手册或官网的url
1.mysql--> https://www.mysql.com/ 2.菜鸟教程--> http://www.runoob.com 3.maven官网--> https://mave ...
- Hive Sql的日常使用笔记
date: 2019-03-22 17:02:37 updated: 2020-04-08 16:00:00 Hive Sql的日常使用笔记 1. distinct 和 group by distin ...
- 使用ML.NET模型生成器来完成图片性别识别
什么是ML.NET? ML.NET 使你能够在联机或脱机场景中将机器学习添加到 .NET 应用程序中. 借助此功能,可以使用应用程序的可用数据进行自动预测. 机器学习应用程序利用数据中的模式来进行预测 ...
- 专攻知识小点——回顾JavaWeb中的servlet(三)
HttpSession基本概述 ** ** 1.HttpSession:是服务器端的技术.和Cookie一样也是服务器和客户端的会话.获得该对象是通过HTTPServletRequest的方法getS ...
- python获取汉字首字母
获取汉字首字母 关注公众号"轻松学编程"了解更多. 应用场景之一:可用于获取名字首字母,在数据库中查询记录时,可以用它来排序输出. from pytz import unicode ...