天天看點

java 多線程程式設計指南 pdf_Java 多線程程式設計(3)-線程活性故障有哪些

java 多線程程式設計指南 pdf_Java 多線程程式設計(3)-線程活性故障有哪些

目錄:

  1. Java 多線程程式設計(入門築基)
  2. Java 多線程程式設計(異步中包含同步)
  3. Java 多線程程式設計(線程活性故障有哪些)
  4. Java 多線程程式設計(“鎖”事碎碎念)
  5. 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
           

根據輸出日志可以分析出,最後每位哲學家均拿到了其左手邊的筷子,且均在等待右手邊的筷子被放下,但此時由于筷子是獨占資源,是以每位哲學家都隻能幹瞪着眼無法吃飯,最終導緻了死鎖

1、死鎖的産生條件

哲學家就餐問題反映了發生死鎖的

「必要條件」

,線程一旦發生死鎖,那麼這些線程及相關的共享資源就一定同時滿足以下條件:

  1. 資源互斥。涉及的資源必須是排他性資源,即每個資源每次隻能由一個線程持有
  2. 資源不可搶奪。涉及的資源隻能由其持有線程主動釋放,其它線程無法從持有線程中主動奪得
  3. 占用并等待其它資源。涉及的線程目前至少已經持有了一個排他性資源,并在申請其它資源,而這些資源同時又被其它線程所持有。在這個資源等待過程中,線程不會主動釋放持有的現有資源
  4. 循環等待資源。在涉及到的所有線程清單内部,每個線程均在互相等待其它線程釋放持有的資源,形成了互相等待的圓形依賴關系。即存在一個處于等待狀态的線程集合 {T1, T2, ..., Tn},其中 Ti 等待的資源被 T(i+1) 占有(i 大于等于 1 小于 n),Tn 等待的資源被 T1 占有

以上條件是死鎖産生的必要條件而非充分條件,即隻要産生了死鎖,以上條件就一定同時成立,但是上訴條件即使同時成立也未必就一定能産生死鎖。例如,對于上訴的第四點,如果線程 T1 等待的資源數大于一,除了等待 T2 主動釋放持有的一份資源外,T1 還可以通過擷取

「循環圈」

外的多餘資源來打破線程間的循環等待關系,進而避免造成死鎖

2、規避死鎖

如果把 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)
    }

}
           

3、死鎖的恢複

死鎖的恢複有着一定難度,原因主要有以下幾點

  • 如果代碼中使用的是内部鎖,或者使用的是顯式鎖的

    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 狀态,但是一直在做無用功

例如,對于上述的哲學家問題,假設某位哲學家比較有禮貌,當其拿起了左手邊的筷子時,如果恰好有其他哲學家需要這根筷子,

「有禮貌的哲學家」

就主動放下筷子,讓給其他哲學家使用。在最極端的情況下,每當有禮貌的哲學家一想要吃飯并拿起左手邊的筷子時,就有其他哲學家需要這根筷子,此時有禮貌的哲學就會一直處于

「拿起筷子-放下筷子-拿起筷子」

這樣一個循環過程中,導緻一直無法吃飯。此時并沒有發生死鎖,但對于有禮貌的哲學家所代表的線程來說就是發生了活鎖

繼續閱讀