来源|为什么是技术
主管|视觉中国
再来说说这期的“废室走板”。
上图是上周末我在看《乐队的夏天》的时候拍的。
这个乐队的名字叫水木年华。我喜欢这个乐队。
我听他们的歌的时候,应该是初中。那时候磁带已经快没气了,进入CD时代。我记得一张CD里有几十首歌。第一次在DVD上听到他们的歌是《一生有你》。听到这首歌的时候,我觉得很干净,很惊艳。
然后逐字抄在自己的歌词本上。
听到这首歌的那个周末,我看着MV,一遍又一遍的学习。当时的DVD有一个功能,可以反复播放某个片段。这首歌是我一句一句学来的。
那时,李健有一双清澈明亮的大眼睛,就像一个湖。我当时还是个小男孩,真想一头扎进他的眼睛里。
这首歌,我更愿意称之为校园民谣的巅峰之一。
十多年后的今天,这个乐队再次出现在我的视野中,但李健不再是其中之一。
他们在乐队的夏季舞台上唱了一首歌《青春再见》,不料被一个自称23岁的胖子骂为“中年人的油腻”,以及“四十多岁的人怎么还能唱出青春永别?”。第一期就被淘汰了。
我被这个操作惊呆了。
这个为什么油腻?为什么四十多岁的人唱不出青春永别?男人到死都是少年,你不知道吗?孩子,他们放音乐的时候你不能说话。
当他们离开舞台的时候,我觉得有点苦涩,有点真的在青春里再见到你的苦涩。
水木年华没有错。错的是这个舞台,不适合他们的歌。
好了,回到文章。
一起看一个问题。前几天有读者给我发了一个链接,说为什么这个链接里的代码是这样运行的。我真的不明白是怎么回事。链接如下:
https://springboot.io/t/topic/1139电码是这样的,给你一张图:
注意,根据他的描述,第10行的许可参数应该是3:
我不知道为什么代码中给出了一个2。但为了保证真实,我直接拿走了,没有做任何改动。我将根据这段代码做一个简单的修改。
知道信号量是做什么的同学可以先看看上面的代码,以及为什么会造成“死锁”。
反正是很哑的低级错误,但是看了好几遍也没看出来。
不知道信号量是干什么的同学,看这里。我先给你科普一下。
Semhore我们一般称之为信号量,用来控制同时访问指定资源的线程数量。
如果你不知道semaphore,你也看不懂上面的代码。我根据代码的逻辑给你举个例子。
比如一个高档停车场,只有三个车位。(这是“指定资源”)
现在里面不停车,那它最多能停几辆车?
是的,门口的剩余车辆标志显示还有3个剩余车位。
这时,三个路人想停下来。
这三条路分别是:转发路、赞美路、欣赏路。
路上的车是为什么哥的劳斯莱斯,赵四的布加迪,刘能和谢广坤开的法拉利,两个基友:
这时从“赞道”来的赵思贤先到,于是停了进去。
门口的车位显示还剩2个车位。
当刘能和谢广坤到达时,他们发现只有两个停车位,所以他们手拉手一起停了下来。
门口的车位显示剩余0个车位。
没多久,我也到了,发现没有停车位。我该怎么办?我必须在门口等着。
过了一会儿,赵四结束了他的生意,开着他的布加迪离开了。
门口的车位显示还剩一个车位。
我赶紧停了下来。
门口的车位显示剩余0个车位。
上面的代码想要描述的就是这样一个东西。
但根据提问者的描述,“运行时,有时只执行线程A,它的线程B和线程C是静默的。”
上面那一幕,赵四的布加迪开进来停了下来,刘能的法拉利,谢广坤和我的劳斯莱斯在后面也停不下来。
是这样的:
你为什么不停下来?他怀疑这是一个僵局。这种怀疑有点荒谬。
让我们先回忆一下死锁的四个必要条件:
互斥条件:一个资源一次只能被一个进程使用,即一个资源只能被一个进程占用一段时间。此时,如果其他进程请求资源,请求进程只能等待。不满意,还有两个车位没用。)
请求和保持条件:一个进程已经保持了至少一个资源,但是它提出了一个新的资源请求,并且该资源已经被其他进程占用。此时,发出请求的进程被阻塞,但它会保留已经获得的资源。(不满意,张三已经占了一个车位,没有再要车位,其他车位也没有被占)
不可剥夺条件3360进程获取的资源在用完之前不能被其他进程强行拿走,即只能由获取资源的进程自己释放。(满意,如果张三的车不出来,这个车位理论上不会被抢走)
循环等待条件3360几个进程形成一个端到端的循环等待资源关系。(不满意,只有刘能、谢广坤和我在等待资源,但没有循环等待。)
这四个条件是死锁的必要条件,也就是说只要有死锁,这些条件都必须成立。
经过分析,我们发现不满足死锁的必要条件。那为什么会有这种现象呢?
我们先按照上面的场景自己写一段代码。
我自己代码下面的程序基本是根据上面截图里的示例代码和上面的故事改的,可以直接复制粘贴:
public class park demo { public static void main(String[]args)throws interrupted exception { Integer park space=3;System.out.println(\ ‘此处有\’ parkSpace \ ‘停车位,先到先得!\’);Semaphore信号量=新信号量(parkSpace,true);thread threada=new thread(new park car(1,\ ‘布加迪\ ‘,信号量),\ ‘赵四\ ‘);Threadthreadb=新线程(新公园车(2,\ ‘法拉利\ ‘,信号量),\ ‘刘能,谢广坤\ ‘);ThreadThreadC=new thread(new park Car(1,\ ‘劳斯莱斯\ ‘,信号量),\ ‘为什么兄弟\ ‘);threadA.startthreadB.startthreadC.start} }类ParkCar实现Runnable { private int n;私弦卡纳姆;专用信号量信号量;public ParkCar(int n,String carName,Semaphore信号量){ this.n=nthis.carName=carNamesemaphore=信号量;} @ override public void run { try { if(semaphore . available permits n){ system . out . println(thread . current thread . getname \ ‘)停车,但是车位不够,就等\ ‘);} semaphore . acquire(n);system . out . println(thread . current thread . getname ‘停了他的’ carName \ ‘,剩余车位: ‘ semaphore . available permitts \ ‘);//模拟停车时间int park time=threadlocalrrandom . current . nextint(1,6);时间单位。seconds . sleep(park time);system . out . println(thread . current thread . getname \ ‘赶走他的\’ carName \ ‘并停止\ ‘ park time \ ‘ hours \ ‘);} catch(Exception e){ e . printstacktrace;} finally { semaphore . release(n);system . out . println(thread . current thread . getname \ ‘离开后,剩余车位为: \ ‘信号量。可用许可证\ ‘);} }}
运行结果如下(由于多线程环境的原因,运行结果可能会有所不同):
这次行动的结果与我们的预期一致。不存在线程阻塞现象。
为什么之前的代码会出现“运行时,有时只执行线程A,而它的线程B和线程C都是静默的”这种现象?
是道德的沦丧还是人性的扭曲?我将带你进入代码:
这种差异反映在获取剩余刀路的方法上。以上是链接里的代码,以下是我自己写的代码。
说实话,我一开始把链接里的代码编译了一分钟,没看出问题。
当我真正把代码粘在IDEA里的时候,我发现线程B第一次执行的时候,线程A和C都可以执行。当线程A首先被执行时,线程B和C将不会被执行。
我不知所措。经过反复分析,我发现这和我的认知不一样!于是我陷入了沉思:
过了一会儿,保洁叔叔来收垃圾,问我:“嗨,帅哥,这瓶红牛喝完了吗?我把瓶子拿走了。”然后我看了一眼屏幕,指着获取剩余许可的那行代码,对我说:“你在这个地方调用了错误的方法。请好好看看方法描述。”
System.out.println(\ ‘剩余可用许可证3360 \ ‘ semaphore . drain permissions);
说完拍了拍肩膀转身走了。当我从大师那里得到启示时,我顿悟了。
由于获取剩余可用许可证的方法是drain permissions,因此在线程A调用完成后,剩余许可证为0,然后在执行释放后,许可证变为1。(后面会有相应的解释)
这时,又是一个公平锁。所以,如果线程B先进入队列,而剩余的许可不足以让线程B运行,那么它将一直等待。c没有机会执行。
将获取剩余可用许可证的方法更改为availablePermits方法后,正常输出是:
这真的是一个小点。这就是当局对旁观者清的原因。
解释我估计很多对信号量不太了解的朋友看完前两部分还是有点不知所措。
没什么,所有的疑惑都会在这一节解开。
在上面的测试案例中,我们只使用了四种信号量方法:
可用许可证:获取剩余的可用许可证。
drain permissions:获取剩余的可用许可证。
Release(int n):释放指定数量的许可证。
Acquire(int n):申请指定数量的许可证。
首先,看看可用许可和排水许可这两种方法之间的区别:
这两个地方的文档描述有点像玩文字游戏。我被粗心大意所欺骗。
仔细看:availablePermits只返回当前可用的许可证数量。而drain permissions是获取和返回,先获取一切再返回。
AvailablePermits只是为了查看还剩多少许可证。drain permissions就是拿走所有剩下的执照。
所以在上面的场景中,这两个方法的返回值是相同的,但是内部的处理是完全不同的:
当我把这一发现报告给清洁大叔时,他温和地笑了,“小伙子,你为什么不在排水许可前面查一下排水的意思呢?”
查了一下,四级留下眼泪:
见名知意。同学们,可见英语对于编程还是很重要的。
接下来,我们来看看放生方式:放生。
方法是释放指定数量的许可证。发布意味着许可证的增加。这就好比刘能和谢广坤开着各自的法拉利驶出停车位,离开停车场。这个时候停车场会多两个车位。
上面的红框是它的主要逻辑。你们自己看看,我就不翻译了。大概意思是,释放了许可证之后,其他等待使用许可证的线程可以看看释放之后的许可证数量是否足够。如果是这样,他们就可以拿到执照跑了。
这个方法的本质在第599到602行的描述中:
这句话至关重要:它意味着执行释放操作的线程不一定是执行获取方法的线程。
开发者需要根据实际场景确保信号量的正确使用。
Operation release这里我们都知道需要放到finally代码块中才能执行。但正是这种认知,是最容易踩坑的地方,也是很难检查出问题的地方。
必须放在finally代码块中,但是这里怎么放有点讲究。
我将用下一节中的示例和获取方法来解释它:
获取方法主要是先关注我的红框。
从这个方法的源代码可以看出,会抛出InterruptException异常。考虑到这一点,我们将在下一节的场景讨论中引入它。
我们仍然将停车场景带入了释放没有被正确使用的坑中。假设我和赵四先停好车。这时,刘能和谢广坤来了,发现停车位不够了。两个好朋友只是等待,不得不一起停下来:
等了一会儿,我们没出来。在门口看车的老爷爷出来对他们说:“我估计你们还得等很久。别等了。走吧。”
于是他们开车走了。
拜托,就这一幕,全码:
public class park demo { public static void main(String[]args)throws interrupted exception { Integer park space=3;System.out.println(\ ‘此处有\’ parkSpace \ ‘停车位,先到先得!\’);Semaphore信号量=新信号量(parkSpace,true);thread threada=new thread(new park car(1,\ ‘布加迪\ ‘,信号量),\ ‘赵四\ ‘);Threadthreadb=新线程(新公园车(2,\ ‘法拉利\ ‘,信号量),\ ‘刘能,谢广坤\ ‘);ThreadThreadC=new thread(new park Car(1,\ ‘劳斯莱斯\ ‘,信号量),\ ‘为什么兄弟\ ‘);threadA.startthreadC.startthreadB.start//模拟大叔劝阻threadB.interrupt} }类ParkCar实现Runnable { private int n;私弦卡纳姆;专用信号量信号量;public ParkCar(int n,String carName,Semaphore信号量){ this.n=nthis.carName=carNamesemaphore=信号量;} @ override public void run { try { if(semaphore . available permits n){ system . out . println(thread . current thread . getname \ ‘)停车,但是车位不够,就等\ ‘);} semaphore . acquire(n);system . out . println(thread . current thread . getname \ ‘停放自己的\’ carName \ ‘,\ ‘剩余车位: \ ‘ semaphore . available permissions \ ‘);//模拟停车时间int park time=threadlocalrrandom . current . nextint(1,6);时间单位。seconds . sleep(park time);system . out . println(thread . current thread . getname \ ‘赶走他的\’ carName \ ‘并停止\ ‘ park time \ ‘ hours \ ‘);} catch(中断异常e){ system . err . println(thread . current thread . getname \ ‘被门口的大叔劝住了。\’);} finally { semaphore . release(n);system . out . println(thread . current thread . getname \ ‘离开后,剩余车位为: \ ‘信号量。可用许可证\ ‘);} }}
看代码没有错,但是运行的时候你会发现有可能出现这样的情况:
为什么哥哥走了之后,剩下的车位变成了五个?我开劳斯莱斯是为了给他们开发停车位吗?
期待日志,我发现刘能和谢广坤离开后,有3个剩余的停车位。
这就是问题所在。
而这个地方对应的代码是这样的:
没有顿悟的感觉。
第50行抛出InterruptedException,导致明显没有获得许可的线程执行release方法,这个方法导致许可增加。
在我们的例子中,刘能和谢广坤的汽车还没有停下来。当他们离开时,门口的显示屏上会增加两个停车位。
这是一个坑,是代码中BUG的潜在区域。
而且非常危险。你觉得你的代码里无缘无故多了几个“许可证”。这意味着运行的线程可能比您预期的要多。非常危险。
那么如何修复呢?
答案已经准备好了。这个地方需要迎头赶上。如果有中断异常,只需返回:
运行,结果是正确的。所有的车都离开后,仍然只有3个停车位:
关于上面的写法还有一个疑问。如果刚领证,就被打断了。我该怎么办?
看源代码。源代码里有答案。
抛出InterruptedException后,所有分配给这个线程的许可证都会被分配给其他想要获取许可证的线程,就像调用release方法一样。
增强释放。分析以上问题后,你会发现问题的原因是没有得到许可的线程调用了release方法。
我觉得这个设定是一个非常容易踩坑的地方。是个大洞!
我们可以在这个问题上增强release方法,只有获取后的线程才能调用release方法。
这招是我在《Java高并发编程详解-深入理解并发核心库》学的:
3.4.4小节是《扩展 Semaphore 增强 release》 :
获取许可证的方法被修改成这样(我只截取其中一个方法),成功获取后放入队列:
修改了内部的release方法,以便在执行前查看当前线程是否在队列中:
还有一个温馨提示:
这本书还是不错的。我推荐给大家。
暂无讨论,说说你的看法吧