欢送来到《并发王者课》,本文是该系列文章中的 第 13 篇。
在上篇文章中,咱们介绍了防止死锁的几种策略。尽管死锁臭名远扬,然而在并发编程中,除了死锁之外,还有一些同样重要的线程活跃性问题值得关注。它们的知名度不高,但破坏性极强,本文将介绍的正是其中的 线程饥饿 和活锁问题。
一、饥饿的产生
所谓线程 饥饿(Starvation) 指的是在多线程的资源竞争中,存在贪心的线程始终锁定资源不开释,其余的线程则始终处于期待状态,然而这个期待是没有后果的,它们会被活活地饿死。
独占者的贪心是饥饿产生的起因之一,概括来说,饥饿个别由上面三种起因导致:
(1)线程被有限阻塞
当取得锁的线程须要执行有限工夫长的操作时(比方 IO 或者有限循环),那么前面的线程将会被 有限阻塞,导致被饿死。
(2)线程优先级升高没有取得 CPU 工夫
当多个竞争的线程被设置优先级之后,优先级越高,线程被给予的 CPU 工夫越多。在某些极其状况下,低优先级的线程可能永远无奈被授予短缺的 CPU 工夫,从而导致被饿死。
(3)线程永远在期待资源
在青铜系列文章中,咱们说过 notify
在发送告诉时,是无奈唤醒指定线程的。当多个线程都处于 wait
时,那么局部线程可能始终无奈被告诉到,以至于挨饿。
二、饥饿与偏心
为了直观体验线程的饥饿,咱们创立了上面的代码。
创立哪吒、兰陵王等四个英雄玩家,他们以竞争的形式打野,杀死野怪能够取得经济收益。
public class StarvationExample {public static void main(String[] args) {final WildMonster wildMonster = new WildMonster();
String[] players = {
"哪吒",
"兰陵王",
"铠",
"典韦"
};
for (String player: players) {Thread playerThread = new Thread(new Runnable() {public void run() {wildMonster.killWildMonster();
}
});
playerThread.setName(player);
playerThread.start();}
}
}
public class WildMonster {public synchronized void killWildMonster() {while (true) {String playerName = Thread.currentThread().getName();
System.out.println(playerName + "斩获野怪!");
try {Thread.sleep(500);
} catch (InterruptedException e) {System.out.println("打野中断");
}
}
}
}
运行后果如下:
哪吒斩获野怪!哪吒斩获野怪!哪吒斩获野怪!哪吒斩获野怪!哪吒斩获野怪!哪吒斩获野怪!哪吒斩获野怪!哪吒斩获野怪!哪吒斩获野怪!哪吒斩获野怪!哪吒斩获野怪!Process finished with exit code 130 (interrupted by signal 2: SIGINT)
从后果中能够看到,在几个线程的运行中,始终只有哪吒能够斩获野怪,其余英雄大刀阔斧等着被饿死。为什么会产生这样的事?
认真看 WildMonster 类中的代码,问题出在 killWildMonster
同步办法中。一旦某个英雄进入该办法后,将始终持有对象锁,其余线程被阻塞而无奈再进入。
当然,解决的办法也很简略,只有突破 独占 即可。比方,咱们在上面的代码中把 Thread.sleep
改成wait
,那么问题将迎刃而解。
public static class WildMonster {public synchronized void killWildMonster() {while (true) {String playerName = Thread.currentThread().getName();
System.out.println(playerName + "斩获野怪!");
try {wait(500);
} catch (InterruptedException e) {System.out.println("打野中断");
}
}
}
}
运行后果如下:
哪吒斩获野怪!铠斩获野怪!兰陵王斩获野怪!典韦斩获野怪!兰陵王斩获野怪!典韦斩获野怪!Process finished with exit code 130 (interrupted by signal 2: SIGINT)
从后果中能够看到,四个英雄都取得了打野的机会,在肯定水平上实现了 偏心 。(备注:wait
会开释锁,但 sleep
不会,对此不了解的能够查看青铜系列文章。)
如何让线程之间公平竞争,是线程问题中的重要话题。尽管咱们无奈保障百分百的偏心,但咱们依然要通过设计肯定的数据结构和应用相应的工具类来减少线程之间的公平性。
对于线程之间的公平性,在本文中重要的是了解它的存在和重要性,对于如何优雅地解决,咱们会在后续的文章中介绍相干的并发工具类。
三、活锁的麻烦
绝对于死锁,你可能对活锁没有那么相熟。然而,活锁所造成的负面影响并不亚于死锁。在后果上,活锁和死锁都是灾难性的,都将会造成应用程序无奈提供失常的服务能力。
所谓 活锁(LiveLock),指的是 两个线程都忙于响应对方的申请,但却不干本人的事。它们一直地反复特定的代码,却一事无成。
不同于死锁,活锁并不会造成线程进入阻塞状态,但它们会原地打转,所以在影响上和死锁类似,程序会进入无线死循环,无奈持续进行。
如果你无奈直观了解活锁是什么,置信你在走路时肯定遇到过上面这种状况。两人相向而行,出于礼貌两人相互让行,让来让去,后果两人依然无奈通行。活锁,也是这个意思。
小结
以上就是对于线程饥饿与活锁的全部内容。在本文中,咱们介绍了线程产生饥饿的起因。看待线程饥饿,没有百分百的计划,但能够尽可能地实现公平竞争。咱们没有在本文列举线程公平性的一些工具类,因为我认为对问题的了解要比解决方案更重要。如果没有对问题的了解,计划在落地时也会呈现知其然而不知其所以然的状况。另外,尽管活锁并不像死锁那样知名度,然而对活锁的失当了解依然十分必要,它是并发常识体系中的一部分。
注释到此结束,祝贺你又上了一颗星✨
夫子的试炼
- 编写代码设置不同线程的优先级,体验线程饥饿并给出解决方案。
延长浏览与参考资料
- 动静图片援用
- 《并发王者课》纲要与更新进度总览
对于作者
关注公众号【庸人技术笑谈】,获取及时文章更新。记录平凡人的技术故事,分享有品质(尽量)的技术文章,偶然也聊聊生存和现实。不贩卖焦虑,不做题目党。
如果本文对你有帮忙,欢送 点赞 、 关注 、 监督 ,咱们一起 从青铜到王者。