目前,多線程編程可以說是在大部分平臺和應用上都需要實現的一個基本需求。本系列文章就來對 Java 平臺下的多線程編程知識進行講解,從概念入門、底層實現到上層應用都會涉及到,預計一共會有五篇文章,希望對你有所幫助😎😎
本篇文章是第三篇,來介紹四種不同類型的線程活性故障現象,這是開發者所必須應對的異常情況
線程活性故障是由於資源稀缺性或者程序自身的問題導致線程一直處於非 Runnable 狀態,或者線程雖然處於 Runnable 狀態但是其要執行的任務一直無法取得進展的一種故障現象
下面就來介紹幾種常見類型的線程活性故障:
- 死鎖
- 鎖死
- 線程飢餓
- 活鎖
一、死鎖
如果多個線程因互相等待對方而被永遠暫停(生命週期狀態爲 Blocked 或者 Waiting),那麼就稱這些線程產生了死鎖(Deadlock)。由於產生死鎖的線程的生命週期狀態永遠是非運行狀態,所以如果沒有外力作用,這些線程所要執行的任務就永遠也無法取得進展
例如,線程 A 在持有鎖 L1 的情況下申請鎖 L2,同時線程 B 在持有鎖 L2 的情況下在申請鎖 L1,而線程 A 和線程 B 各自要求只有在取得對方的鎖後才能釋放持有的鎖,這就導致了兩個鎖都將處於無限等待的狀態,此時死鎖就發生了
有關死鎖的一個經典問題是哲學家就餐問題。五位哲學家圍着一張圓桌,每位哲學家之間均放着一根筷子,即一共有五根筷子。每位哲學家要麼處於思考狀態,要麼是在喫飯。喫飯前,每位哲學家均會先拿起左手邊的筷子,再拿起右手邊的筷子,只有當手上持有了一雙筷子時哲學家才能夠喫飯,且除非本次喫飯行爲完成,否則哲學家不會放下手中已持有的筷子。哲學家喫完飯後就會放下手中的筷子,再次思考一段時間後再進行喫飯
在這個問題中,每位哲學家就相當於一個線程,每根筷子就相當於多條線程間的共享資源。且筷子明顯是一個排他性資源,因爲每根筷子每次只能由一位哲學家持有,因此哲學家在拿起筷子前需要先取得筷子對應的鎖。由於筷子和哲學家的數量相等,而每位哲學家需要的筷子數量是現有的兩倍,所以發生死鎖的可能性還是很大的
我們可以用一段程序來模擬並驗證上述的情況
先對筷子 Chopstick 進行定義,其能被操作的行爲只有兩種,即:拿起和放下
enum class ChopstickStatus {
UP,
Down
}
data class Chopstick(val id: Int) {
var status = ChopstickStatus.Down
private set
fun pickUp() {
status = ChopstickStatus.UP
}
fun putDown() {
status = ChopstickStatus.Down
}
}
每位哲學家 Philosopher 均對應一個唯一的標識 id,一根左手邊的筷子 left,一根右手邊的筷子 right。會不間斷地進行“思考”和“喫飯”兩種行爲,每種行爲均包含一段隨機的時間間隔(隨機的線程休眠)
private object Tools {
fun randomSleep(max: Long = 30) {
val realMax = max.coerceAtLeast(1)
ThreadLocalRandom.current().nextLong(if (realMax == 1L) 0 else 1, max + 1)
}
}
data class Philosopher(val id: Int, val left: Chopstick, val right: Chopstick) : Thread("Philosopher-$id") {
override fun run() {
while (true) {
think()
eat()
}
}
private fun eat() {
synchronized(left) {
println("$name 拿起了左邊的筷子: " + left.id)
left.pickUp()
synchronized(right) {
println("$name 拿起了右邊的筷子: " + right.id)
right.pickUp()
println("$name 開始喫飯.....")
Tools.randomSleep(10)
println("$name 喫飯結束!!!!!!!!!!")
right.putDown()
}
left.putDown()
}
}
private fun think() {
println("$name 思考中....")
Tools.randomSleep(100)
}
}
然後,我們創建五根筷子,並將每根筷子按順序分配給每位哲學家,然後就啓動哲學家的思考和喫飯行爲(啓動線程)
/**
* 作者:leavesC
* 時間:2020/8/14 14:46
* 描述:
* GitHub:https://github.com/leavesC
*/
fun main() {
val philosopherNumber = 5
val chopstickList = mutableListOf<Chopstick>()
for (i in 0 until philosopherNumber) {
chopstickList.add(Chopstick(i))
}
val philosopherList = mutableListOf<Philosopher>()
for (index in 0 until philosopherNumber) {
val left = chopstickList[index]
val right = chopstickList.getOrNull(index - 1) ?: chopstickList.last()
philosopherList.add(Philosopher(index, left, right))
}
philosopherList.forEach {
println(it.name + " 左手邊的筷子是:" + it.left + " 右手邊的筷子是:" + it.right)
}
philosopherList.forEach {
it.start()
}
}
最後,運行程序後,只要我們爲哲學家設定每次思考和喫飯的耗時時間不要太長,那麼應該就能很快看到程序沒有繼續輸出日誌了,似乎被卡住了,此時即發生了死鎖
Philosopher-0 左手邊的筷子是:Chopstick(id=0) 右手邊的筷子是:Chopstick(id=4)
Philosopher-1 左手邊的筷子是:Chopstick(id=1) 右手邊的筷子是:Chopstick(id=0)
Philosopher-2 左手邊的筷子是:Chopstick(id=2) 右手邊的筷子是:Chopstick(id=1)
Philosopher-3 左手邊的筷子是:Chopstick(id=3) 右手邊的筷子是:Chopstick(id=2)
Philosopher-4 左手邊的筷子是:Chopstick(id=4) 右手邊的筷子是:Chopstick(id=3)
Philosopher-0 思考中....
Philosopher-1 思考中....
Philosopher-2 思考中....
Philosopher-3 思考中....
Philosopher-4 思考中....
Philosopher-0 拿起了左邊的筷子: 0
Philosopher-2 拿起了左邊的筷子: 2
Philosopher-3 拿起了左邊的筷子: 3
Philosopher-0 拿起了右邊的筷子: 4
Philosopher-0 開始喫飯.....
Philosopher-0 喫飯結束!!!!!!!!!!
Philosopher-2 拿起了右邊的筷子: 1
Philosopher-2 開始喫飯.....
Philosopher-2 喫飯結束!!!!!!!!!!
Philosopher-0 思考中....
Philosopher-0 拿起了左邊的筷子: 0
Philosopher-4 拿起了左邊的筷子: 4
Philosopher-3 拿起了右邊的筷子: 2
Philosopher-3 開始喫飯.....
Philosopher-3 喫飯結束!!!!!!!!!!
Philosopher-3 思考中....
Philosopher-4 拿起了右邊的筷子: 3
Philosopher-4 開始喫飯.....
Philosopher-4 喫飯結束!!!!!!!!!!
Philosopher-2 思考中....
Philosopher-4 思考中....
Philosopher-1 拿起了左邊的筷子: 1
Philosopher-2 拿起了左邊的筷子: 2
Philosopher-3 拿起了左邊的筷子: 3
Philosopher-0 拿起了右邊的筷子: 4
Philosopher-0 開始喫飯.....
Philosopher-0 喫飯結束!!!!!!!!!!
Philosopher-0 思考中....
Philosopher-0 拿起了左邊的筷子: 0
Philosopher-4 拿起了左邊的筷子: 4
根據輸出日誌可以分析出,最後每位哲學家均拿到了其左手邊的筷子,且均在等待右手邊的筷子被放下,但此時由於筷子是獨佔資源,所以每位哲學家都只能幹瞪着眼無法喫飯,最終導致了死鎖
二、死鎖的產生條件
哲學家就餐問題反映了發生死鎖的必要條件,線程一旦發生死鎖,那麼這些線程及相關的共享資源就一定同時滿足以下條件:
- 資源互斥。涉及的資源必須是排他性資源,即每個資源每次只能由一個線程持有
- 資源不可搶奪。涉及的資源只能由其持有線程主動釋放,其它線程無法從持有線程中主動奪得
- 佔用並等待其它資源。涉及的線程當前至少已經持有了一個排他性資源,並在申請其它資源,而這些資源同時又被其它線程所持有。在這個資源等待過程中,線程不會主動釋放持有的現有資源
- 循環等待資源。在涉及到的所有線程列表內部,每個線程均在互相等待其它線程釋放持有的資源,形成了互相等待的圓形依賴關係。即存在一個處於等待狀態的線程集合 {T1, T2, ..., Tn},其中 Ti 等待的資源被 T(i+1) 佔有(i 大於等於 1 小於 n),Tn 等待的資源被 T1 佔有
以上條件是死鎖產生的必要條件而非充分條件,即只要產生了死鎖,以上條件就一定同時成立,但是上訴條件即使同時成立也未必就一定能產生死鎖。例如,對於上訴的第四點,如果線程 T1 等待的資源數大於一,除了等待 T2 主動釋放持有的一份資源外,T1 還可以通過獲取循環圈外的多餘資源來打破線程間的循環等待關係,從而避免造成死鎖
三、規避死鎖
如果把 Java 平臺下的鎖(Lock)當做一種資源,那麼這種資源就正好符合“資源互斥”和“資源不可搶奪”的要求,在這種情況下,產生死鎖的代碼特徵就是在持有一個鎖的情況下去申請另外一個鎖,這通常意味着鎖的嵌套。但是,一個線程在已經持有一個鎖的情況下再次申請這個鎖並不會導致死鎖,這是因爲 Java 中的鎖都是可重入的(Reentrant),這種情形下線程重複申請某個鎖是可以成功的
從上訴的四個發生死鎖的必要條件來反推,我們只要消除死鎖產生的任意一個必要條件就可以規避死鎖了。由於鎖具有排他性且只能由其持有線程來主動釋放,因此由鎖導致的死鎖只能從消除“佔用並等待資源”和消除“循環等待資源”這兩個方向入手。以下就來介紹基於這兩個思路來規避死鎖的方法
1、粗鎖法
粗鎖法即使用粗粒度的鎖來代替多個鎖。“佔用並等待資源”這個條件隱含的情況即:線程在持有一個鎖的同時還去申請另一個鎖。那麼,只要採用一個粒度較粗的鎖來替代原先粒度較細的鎖,使得涉及的資源都只需要申請一個鎖就可以獲得,那麼就可以避免死鎖
對應上訴的哲學家就餐問題,只要將 Philosopher 拿左手邊筷子和拿右手邊筷子的行爲統一放到同個鎖內,就可以消除“佔用並等待資源”和“循環等待資源”這兩個條件了
data class Philosopher(val id: Int, val left: Chopstick, val right: Chopstick) : Thread("Philosopher-$id") {
companion object {
private val LOCK = Object()
}
override fun run() {
while (true) {
think()
eat()
}
}
private fun eat() {
synchronized(LOCK) {
println("$name 拿起了左邊的筷子: " + left.id)
left.pickUp()
println("$name 拿起了右邊的筷子: " + right.id)
right.pickUp()
println("$name 開始喫飯.....")
Tools.randomSleep(10)
println("$name 喫飯結束!!!!!!!!!!")
right.putDown()
left.putDown()
}
}
private fun think() {
println("$name 思考中....")
Tools.randomSleep(100)
}
}
粗鎖法的缺點就是它明顯降低了併發性並可能導致資源浪費。修改過後的代碼,每次只有一位哲學家能夠喫飯。如果每位哲學家喫飯的耗時相對其思考的時間要長得多,那麼在持有筷子的哲學家喫飯結束前,有可能其他哲學家都已經處於等待筷子的狀態了(即鎖的爭用程度比較高,多個線程由於申請不到鎖而被暫停,每次鎖的爭奪可能會經歷多次線程上下文切換)。而如果每位哲學家喫飯的耗時相對其思考的時間要短得多,那麼就有可能在非持有筷子的哲學家結束思考前,持有筷子的哲學家就已經喫飯結束了(即鎖的爭用比較低,每次只有一個線程來申請鎖,此時就不會由於申請鎖而導致線程上下文切換)
即使鎖的爭用程度比較低,一位哲學家在喫飯的時候也僅需要佔用兩根筷子,剩下的三根筷子本來還可以提供給另外一位哲學家使用,此時採用粗鎖法就明顯導致了資源的浪費。因此,粗鎖法的適用範圍較爲有限
2、鎖排序法
鎖排序法的思路是:對所有鎖按照一定規則進行排序,所有線程在申請鎖之前均按照先後順序進行申請,以此來消除“循環等待資源”這個條件,從而來規避死鎖
例如,對上訴的哲學家問題進行簡單化。假設哲學家的數量是 2。哲學家1的左手邊是筷子2,右手邊是筷子1;哲學家2的左手邊是筷子1,右手邊是筷子2。當兩位哲學家同時拿起左手邊的筷子時,此時就會發生死鎖。而如果對筷子的申請順序進行要求,要求哲學家需要先拿起 ID 較小的筷子才能去申請 ID 較大的筷子,那麼此時先拿到筷子1的哲學家就可以無競爭地拿到筷子2,從而避免了“循環等待資源”的情況
data class Philosopher(val id: Int, val left: Chopstick, val right: Chopstick) : Thread("Philosopher-$id") {
private val one: Chopstick
private val theOther: Chopstick
init {
//每位哲學家都對其左右兩邊的筷子進行排序
//都按照“先取ID小的筷子再取ID大的筷子”的這種規則來拿筷子
if (left.id < right.id) {
one = left
theOther = right
} else {
one = right
theOther = left
}
}
override fun run() {
while (true) {
think()
eat()
}
}
private fun eat() {
synchronized(one) {
println("$name 拿起了左邊的筷子: " + left.id)
left.pickUp()
synchronized(theOther) {
println("$name 拿起了右邊的筷子: " + right.id)
right.pickUp()
println("$name 開始喫飯.....")
Tools.randomSleep(10)
println("$name 喫飯結束!!!!!!!!!!")
right.putDown()
left.putDown()
}
}
}
private fun think() {
println("$name 思考中....")
Tools.randomSleep(100)
}
}
3、資源限時申請
避免死鎖的另一種方法是在申請資源時設定一個超時時間,避免無限制地等待資源,從而消除“佔用並等待資源”這種情況。當等待時間超出既定的限制時,釋放已持有的資源(哲學家放下左手邊的筷子轉而去繼續思考)先給其它線程使用,待後續再重新申請資源
data class Philosopher(val id: Int, val left: Chopstick, val right: Chopstick) : Thread("Philosopher-$id") {
companion object {
private val LOCK_MAP = ConcurrentHashMap<Chopstick, ReentrantLock>()
}
init {
LOCK_MAP.putIfAbsent(left, ReentrantLock())
LOCK_MAP.putIfAbsent(right, ReentrantLock())
}
override fun run() {
while (true) {
think()
eat()
}
}
private fun eat() {
val leftLock = LOCK_MAP[left]!!
val leftLockAcquired = leftLock.tryLock(10, TimeUnit.MILLISECONDS)
if (!leftLockAcquired) {
return
}
val rightLock = LOCK_MAP[right]!!
val rightLockAcquired = rightLock.tryLock(10, TimeUnit.MILLISECONDS)
if (!rightLockAcquired) {
leftLock.unlock()
return
}
println("$name 拿起了左邊的筷子: " + left.id)
left.pickUp()
println("$name 拿起了右邊的筷子: " + right.id)
right.pickUp()
println("$name 開始喫飯.....")
Tools.randomSleep(10)
println("$name 喫飯結束!!!!!!!!!!")
right.putDown()
left.putDown()
leftLock.unlock()
rightLock.unlock()
}
private fun think() {
println("$name 思考中....")
Tools.randomSleep(100)
}
}
四、死鎖的恢復
死鎖的恢復有着一定難度,原因主要有以下幾點
- 如果代碼中使用的是內部鎖,或者使用的是顯式鎖的
Lock.lock()
方法,那麼這些鎖導致的死鎖是無法恢復的,此時只能通過重啓 Java 虛擬機來停止程序 - 可以通過定義一個工作者線程專門用於檢測和恢復死鎖。該線程定時檢測系統中是否存在死鎖,如果存在,則選擇一個死鎖線程向其發送中斷。該中斷使得相應的死鎖線程被喚醒並拋出 InterruptedException 異常,死鎖線程捕獲到 InterruptedException 異常後主動釋放已持有的資源,從而“消除並等待資源”這個條件。如果該死鎖線程釋放已持有的線程後依然存在死鎖,工作者線程就繼續選擇一個死鎖線程進行中斷處理,直到消除死鎖。這種方法依賴於發生死鎖的線程能夠響應中斷,而能響應中斷的同時並釋放已持有的資源就意味着在一開始我們就考慮到了可能會發生死鎖,那麼我們應該在一開始就做好死鎖的預防,而不是使死鎖線程支持死鎖的恢復處理
- 即使死鎖線程能夠在響應中斷的同時並釋放已持有的資源,那麼檢測死鎖的工作者線程應該按照什麼順序來中斷死鎖線程依然是個問題,且被中斷的死鎖線程可能會丟失其之前已經完成的計算任務,從而導致各種意想不到的情況
這裏根據第一節內容中會發生死鎖的哲學家問題,來嘗試恢復死鎖
定義一個死鎖檢測線程 DeadlockDetector,它會每隔一段時間定時檢測當前系統是否發生了死鎖,如果發生了的話,則從涉及到死鎖的所有線程中選擇一個線程向其發送中斷請求,被中斷的線程內部需要捕獲中斷異常,然後自動釋放其持有的資源,嘗試將資源讓給其它線程使用,從而打破佔用並等待其它資源和資源循環等待兩個條件
/**
* 作者:leavesC
* 時間:2020/8/14 16:53
* 描述:
* GitHub:https://github.com/leavesC
*/
class DeadlockDetector(monitorInterval: Long) : Thread("DeadlockDetector") {
companion object {
private val tmb = ManagementFactory.getThreadMXBean()
//獲取發生死鎖時涉及到的所有線程
fun findDeadlockedThreads(): Array<ThreadInfo> {
val ids = tmb.findDeadlockedThreads()
return if (tmb.findDeadlockedThreads() == null) arrayOf() else tmb.getThreadInfo(ids) ?: arrayOf()
}
fun findThreadById(threadId: Long): Thread? {
for (thread in getAllStackTraces().keys) {
if (thread.id == threadId) {
return thread
}
}
return null
}
//向線程發送中斷
fun interruptThread(threadId: Long): Boolean {
val thread = findThreadById(threadId)
if (thread != null) {
thread.interrupt()
return true
}
return false
}
}
//檢測週期,單位爲毫秒
private val monitorInterval: Long
init {
isDaemon = true
this.monitorInterval = monitorInterval
}
override fun run() {
var threadInfoList: Array<ThreadInfo>
var ti: ThreadInfo?
var i = 0
try {
while (true) {
threadInfoList = findDeadlockedThreads()
if (threadInfoList.isNotEmpty()) {
ti = threadInfoList[i++ % threadInfoList.size]
interruptThread(ti.threadId)
continue
} else {
i = 0
}
sleep(monitorInterval)
}
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
}
Philosopher 通過 Lock.lockInterruptibly()
方法來申請鎖,該方法可以響應中斷。此時就可以在捕獲到中斷異常時自動釋放已持有的資源
data class Philosopher(val id: Int, val left: Chopstick, val right: Chopstick) : Thread("Philosopher-$id") {
companion object {
private val LOCK_MAP = ConcurrentHashMap<Chopstick, ReentrantLock>()
}
init {
LOCK_MAP.putIfAbsent(left, ReentrantLock())
LOCK_MAP.putIfAbsent(right, ReentrantLock())
}
override fun run() {
while (true) {
think()
eat()
}
}
private fun eat() {
val leftLock = LOCK_MAP[left]!!
try {
leftLock.lockInterruptibly()
} catch (e: InterruptedException) {
e.printStackTrace()
println("$name ==========放棄等待左邊的筷子: " + left.id)
return
}
println("$name 拿起了左邊的筷子: " + left.id)
left.pickUp()
val rightLock = LOCK_MAP[right]!!
try {
rightLock.lockInterruptibly()
} catch (e: InterruptedException) {
e.printStackTrace()
println("$name ==========放棄等待右邊的筷子: " + right.id)
left.putDown()
println("$name ====================放下已持有的左邊的筷子: " + left.id)
leftLock.unlock()
return
}
println("$name 拿起了右邊的筷子: " + right.id)
right.pickUp()
println("$name 開始喫飯.....")
Tools.randomSleep(10)
println("$name 喫飯結束!!!!!!!!!!")
left.putDown()
right.putDown()
leftLock.unlock()
rightLock.unlock()
}
private fun think() {
println("$name 思考中....")
Tools.randomSleep(100)
}
}
/**
* 作者:leavesC
* 時間:2020/8/14 17:16
* 描述:
* GitHub:https://github.com/leavesC
*/
fun main() {
val philosopherNumber = 5
val chopstickList = mutableListOf<Chopstick>()
for (i in 0 until philosopherNumber) {
chopstickList.add(Chopstick(i))
}
val philosopherList = mutableListOf<Philosopher>()
for (index in 0 until philosopherNumber) {
val left = chopstickList[index]
val right = chopstickList.getOrNull(index - 1) ?: chopstickList.last()
philosopherList.add(Philosopher(index, left, right))
}
philosopherList.forEach {
println(it.name + " 左手邊的筷子是:" + it.left + " 右手邊的筷子是:" + it.right)
}
philosopherList.forEach {
it.start()
}
DeadlockDetector(2000).start()
}
從日誌輸出可以看出來,線程 Philosopher-4 在收到中斷請求時釋放了其持有的資源,但很快又發生了死鎖,因爲線程 Philosopher-4 釋放的資源可以由所有需要的線程進行搶奪,可能在 線程 Philosopher-4 剛釋放了持有的資源時又馬上自己搶佔到了該資源。最極端的情況就是:每次收到中斷的線程均釋放了其持有的資源,但隨後又馬上自己搶佔到了該資源,接着該線程(或者其它線程)又收到中斷請求,又釋放持有的資源,又自己搶佔到該資源……如此循環往復。這種情況下所有線程依然無法獲得依賴的目標資源,反而由於反覆的鎖申請和鎖釋放操作造成多次的線程上下文切換。且釋放鎖可能會導致線程之前的任務被無效化。所以說,死鎖的恢復實際意義不大
Philosopher-0 左手邊的筷子是:Chopstick(id=0) 右手邊的筷子是:Chopstick(id=4)
Philosopher-1 左手邊的筷子是:Chopstick(id=1) 右手邊的筷子是:Chopstick(id=0)
Philosopher-2 左手邊的筷子是:Chopstick(id=2) 右手邊的筷子是:Chopstick(id=1)
Philosopher-3 左手邊的筷子是:Chopstick(id=3) 右手邊的筷子是:Chopstick(id=2)
Philosopher-4 左手邊的筷子是:Chopstick(id=4) 右手邊的筷子是:Chopstick(id=3)
Philosopher-0 思考中....
Philosopher-1 思考中....
Philosopher-2 思考中....
Philosopher-3 思考中....
Philosopher-4 思考中....
Philosopher-2 拿起了左邊的筷子: 2
Philosopher-3 拿起了左邊的筷子: 3
Philosopher-4 拿起了左邊的筷子: 4
Philosopher-0 拿起了左邊的筷子: 0
Philosopher-2 拿起了右邊的筷子: 1
Philosopher-2 開始喫飯.....
Philosopher-2 喫飯結束!!!!!!!!!!
Philosopher-2 思考中....
Philosopher-1 拿起了左邊的筷子: 1
Philosopher-3 拿起了右邊的筷子: 2
Philosopher-3 開始喫飯.....
Philosopher-3 喫飯結束!!!!!!!!!!
Philosopher-3 思考中....
Philosopher-2 拿起了左邊的筷子: 2
Philosopher-3 拿起了左邊的筷子: 3
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at thread.Philosopher.eat(DeadLockDemo.kt:71)
at thread.Philosopher.run(DeadLockDemo.kt:52)
Philosopher-4 ==========放棄等待右邊的筷子: 3
Philosopher-4 ====================放下已持有的左邊的筷子: 4
Philosopher-4 思考中....
Philosopher-0 拿起了右邊的筷子: 4
Philosopher-0 開始喫飯.....
Philosopher-0 喫飯結束!!!!!!!!!!
Philosopher-0 思考中....
Philosopher-1 拿起了右邊的筷子: 0
Philosopher-1 開始喫飯.....
Philosopher-4 拿起了左邊的筷子: 4
Philosopher-1 喫飯結束!!!!!!!!!!
Philosopher-1 思考中....
Philosopher-0 拿起了左邊的筷子: 0
Philosopher-2 拿起了右邊的筷子: 1
Philosopher-2 開始喫飯.....
Philosopher-2 喫飯結束!!!!!!!!!!
Philosopher-2 思考中....
Philosopher-3 拿起了右邊的筷子: 2
Philosopher-1 拿起了左邊的筷子: 1
Philosopher-3 開始喫飯.....
Philosopher-3 喫飯結束!!!!!!!!!!
Philosopher-3 思考中....
Philosopher-2 拿起了左邊的筷子: 2
Philosopher-3 拿起了左邊的筷子: 3
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at thread.Philosopher.eat(DeadLockDemo.kt:71)
at thread.Philosopher.run(DeadLockDemo.kt:52)
Philosopher-4 ==========放棄等待右邊的筷子: 3
Philosopher-4 ====================放下已持有的左邊的筷子: 4
Philosopher-4 思考中....
Philosopher-0 拿起了右邊的筷子: 4
Philosopher-0 開始喫飯.....
Philosopher-0 喫飯結束!!!!!!!!!!
Philosopher-0 思考中....
Philosopher-4 拿起了左邊的筷子: 4
Philosopher-0 拿起了左邊的筷子: 0
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at thread.Philosopher.eat(DeadLockDemo.kt:71)
at thread.Philosopher.run(DeadLockDemo.kt:52)
Philosopher-4 ==========放棄等待右邊的筷子: 3
Philosopher-4 ====================放下已持有的左邊的筷子: 4
Philosopher-4 思考中....
Philosopher-0 拿起了右邊的筷子: 4
Philosopher-0 開始喫飯.....
Philosopher-0 喫飯結束!!!!!!!!!!
Philosopher-0 思考中....
Philosopher-0 拿起了左邊的筷子: 0
Philosopher-0 拿起了右邊的筷子: 4
Philosopher-0 開始喫飯.....
Philosopher-0 喫飯結束!!!!!!!!!!
Philosopher-0 思考中....
Philosopher-0 拿起了左邊的筷子: 0
Philosopher-0 拿起了右邊的筷子: 4
Philosopher-0 開始喫飯.....
Philosopher-0 喫飯結束!!!!!!!!!!
Philosopher-0 思考中....
Philosopher-0 拿起了左邊的筷子: 0
Philosopher-0 拿起了右邊的筷子: 4
Philosopher-0 開始喫飯.....
Philosopher-0 喫飯結束!!!!!!!!!!
Philosopher-0 思考中....
Philosopher-0 拿起了左邊的筷子: 0
Philosopher-4 拿起了左邊的筷子: 4
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at thread.Philosopher.eat(DeadLockDemo.kt:71)
at thread.Philosopher.run(DeadLockDemo.kt:52)
Philosopher-4 ==========放棄等待右邊的筷子: 3
Philosopher-4 ====================放下已持有的左邊的筷子: 4
Philosopher-4 思考中....
Philosopher-4 拿起了左邊的筷子: 4
····
五、鎖死
等待線程由於喚醒其所需的條件永遠無法成立,或者是其它線程無法喚醒這個線程導致其一直處於非運行狀態(線程並未終止)從而任務一直取得進展,那麼我們稱這個線程被鎖死
鎖死和死鎖之間有着共同的外在表現:故障線程一直處於非運行狀態而使得其任務無法進展。死鎖針對的是多個線程,而鎖死可能只是作用在一個線程上。例如,一個調用了 Object.wait()
處於等待狀態的線程,由於發生異常或者是代碼缺陷,導致一直沒有外部線程調用 Object.notify()
方法來喚醒等待線程,使得線程一直處於等待狀態無法運行,此時就可以說該線程被鎖死
鎖死和死鎖的產生條件是不同的,即便是在產生死鎖的所有必要條件都不成立的情況下(此時死鎖不可能發生),鎖死仍可能出現。因此應對死鎖的辦法未必能夠用來避免鎖死現象的發生。按照鎖死產生的條件來分,鎖死包括信號丟失鎖死和嵌套監視器鎖死
1、信號丟失鎖死
信號丟失鎖死是由於沒有相應的通知線程來喚醒等待線程而使等待線程一直處於等待狀態的一種活性故障
例如,某個等待線程在執行 Object.wait()
前沒有對保護條件進行判斷,而此時保護條件實際上已經成立了,然而此後可能並無其他線程會來喚醒等待線程,因爲在等待線程獲得 Object 內部鎖之前保護條件已經是處於成立狀態了,這就使得等待線程一直處於等待狀態,其任務一直無法取得進展
信號丟失鎖死的另外一個常見例子是由於 CountDownLatch.countDown()
沒有放在 finally 塊中,而如果 CountDownLatch.countDown()
的執行線程運行時拋出未捕獲的異常時, CountDownLatch.await()
的執行線程就會一直處於等待狀態從而任務一直無法取得進展
例如,對於以下代碼,當 ServiceB 拋出異常時,main 線程就會由於一直無法收到喚醒通知從而一直處於等待狀態
/**
* 作者:leavesC
* 時間:2020/7/31 15:48
* 描述:
* GitHub:https://github.com/leavesC
*/
fun main() {
val serviceManager = ServicesManager()
serviceManager.startServices()
println("等待所有 Services 執行完畢")
val allSuccess = serviceManager.checkState()
println("執行結果: $allSuccess")
}
class ServicesManager {
private val countDownLatch = CountDownLatch(2)
private val serviceList = mutableListOf<AbstractService>()
init {
serviceList.add(ServiceA("ServiceA", countDownLatch))
serviceList.add(ServiceB("ServiceB", countDownLatch))
}
fun startServices() {
serviceList.forEach {
it.start()
}
}
fun checkState(): Boolean {
countDownLatch.await()
return serviceList.find { !it.checkState() } == null
}
}
abstract class AbstractService(private val countDownLatch: CountDownLatch) {
private var success = false
abstract fun doTask(): Boolean
fun start() {
thread {
// try {
// success = doTask()
// } finally {
// countDownLatch.countDown()
// }
success = doTask()
countDownLatch.countDown()
}
}
fun checkState(): Boolean {
return success
}
}
class ServiceA(private val serviceName: String, countDownLatch: CountDownLatch) : AbstractService(countDownLatch) {
override fun doTask(): Boolean {
Thread.sleep(2000)
println("${serviceName}執行完畢")
return true
}
}
class ServiceB(private val serviceName: String, countDownLatch: CountDownLatch) : AbstractService(countDownLatch) {
override fun doTask(): Boolean {
Thread.sleep(3000)
if (Random.nextBoolean()) {
throw RuntimeException("$serviceName failed")
} else {
println("${serviceName}執行完畢")
}
return true
}
}
2、嵌套監視器鎖死
嵌套監視器鎖死是嵌套鎖導致等待線程永遠無法被喚醒的一種活性故障
來看以下僞代碼。假設存在一個等待線程,其先後持有了 monitorX 和 monitorY 兩個不同的鎖,當等待線程監測到當前執行條件不成立時,調用了 monitorY.wait()
等待通知線程來喚醒自身,並同時釋放了鎖 monitorY
synchronized(monitorX) {
//...
synchronized(monitorY) {
while (!somethingOk) {
monitorY.wait()
}
//執行目標行爲
}
}
相應的通知線程其僞代碼如下所示。通知線程需要持有了 monitorX 和 monitorY 兩個鎖才能執行到 monitorY.notifyAll()
這行代碼來喚醒等待線程。而等待線程執行 monitorY.wait()
時僅會釋放 monitorY,而不會釋放 monitorX。這使得通知線程由於一直獲得 monitorX, 從而導致等待線程一直無法被喚醒而一直處於 BLOCKED 狀態
synchronized(monitorX) {
//...
synchronized(monitorY) {
//...
somethingOk = true
monitorY.notifyAll()
//...
}
}
這種由於嵌套鎖導致通知線程始終無法喚醒等待線程的活性故障就被稱爲嵌套監視器鎖死
六、線程飢餓
線程飢餓是指線程一直無法獲得所需資源從而導致任務無法取得進展的一種活性故障現象
產生線程飢餓的一種情況是:線程一直沒有被分配到處理器時間片。這種情況一般是由於處理器時間片一直被高優先級的線程搶佔,低優先級的線程一直無法獲得運行機會,此時即發生了線程飢餓現象。Thread
類提供了修改線程優先級的成員方法setPriority(Int)
,定義了整數一到十之間的十個優先級級別。不同的操作系統會有不同的線程優先級等級,JVM 會把這 Thread 類的十個優先級級別映射到具體的操作系統所定義的線程優先級關係上。但是我們所設置的線程優先級對線程調度器來說只是一個建議,當我們將一個線程設置爲高優先級時,既可能會被線程調度器忽略,也可能會使該線程過度優先執行而別的線程一直得不到處理器時間片,從而導致線程飢餓。因此我們應該儘量避免修改線程的優先級
把鎖看做一種資源,那麼死鎖也是一種線程飢餓。死鎖的結果是所有故障線程都無法獲得其所需的全部鎖,從而使得其任務一直無法取得進展,這就相當於線程無法獲得所需的全部資源從而導致任務無法取得進展,即產生了線程飢餓
發生線程飢餓並不一定同時存在死鎖。因爲線程飢餓可能只發生在一個線程上(例如上述的低優先級線程無法獲得時間片),且即使是同時發生在多個線程上,也可能並不滿足死鎖發生的必要條件之一:循環等待資源,因爲此時涉及到的多個線程所等待的資源可能並沒有相互依賴關係
七、活鎖
活鎖指的是任務和任務的執行線程均沒有被阻塞,但由於某些條件沒有滿足,導致線程一直在重複嘗試—失敗—嘗試的過程,任務一直無法取得進展。也就是說,產生活鎖的線程雖然處於 Runnable 狀態,但是一直在做無用功
例如,對於上述的哲學家問題,假設某位哲學家“比較有禮貌”,當其拿起了左手邊的筷子時,如果恰好有其他哲學家需要這根筷子,有禮貌的哲學家就主動放下筷子,讓給其他哲學家使用。在最極端的情況下,每當有禮貌的哲學家一想要喫飯並拿起左手邊的筷子時,就有其他哲學家需要這根筷子,此時有禮貌的哲學就會一直處於拿起筷子-放下筷子-拿起筷子這樣一個循環過程中,導致一直無法喫飯。此時並沒有發生死鎖,但對於有禮貌的哲學家所代表的線程來說就是發生了活鎖