Java線程的死鎖和活鎖

一、概覽

當多線程幫助咱們提升應用性能的同時,它同時也帶來一些問題,本文咱們將藉助幾個小例子看下兩個問題,死鎖和活鎖。java

二、死鎖

2.一、什麼是死鎖

死鎖發生在當兩個或多個線程一直在等待另外一個線程持有的鎖或資源的時候。這會致使一個程序可能會被拖垮或者直接掛掉,由於線程們都不能繼續工做了。多線程

經典的哲學家進餐問題很是好的展現了多線程下的同步問題而且常常被用來看成死鎖的例子。併發

2.2 死鎖舉例

首先,咱們看一個簡單的Java例子來理解死鎖。性能

在這個例子中,咱們建立兩個線程,T1和T2。線程T1調用operation1,線程T2調用operation2。測試

爲了完成操做,線程T1須要先獲取到lock1再獲取到lock2,而後此時線程T2須要先獲取到lock2再獲取到lock1。所以兩個線程都在以相反的順序獲取鎖。ui

如今,咱們寫一下DeadlockExample:線程

public class DeadlockExample {
 
    private Lock lock1 = new ReentrantLock(true);
    private Lock lock2 = new ReentrantLock(true);
 
    public static void main(String[] args) {
        DeadlockExample deadlock = new DeadlockExample();
        new Thread(deadlock::operation1, "T1").start();
        new Thread(deadlock::operation2, "T2").start();
    }
 
    public void operation1() {
        lock1.lock();
        print("lock1 acquired, waiting to acquire lock2.");
        sleep(50);
 
        lock2.lock();
        print("lock2 acquired");
 
        print("executing first operation.");
 
        lock2.unlock();
        lock1.unlock();
    }
 
    public void operation2() {
        lock2.lock();
        print("lock2 acquired, waiting to acquire lock1.");
        sleep(50);
 
        lock1.lock();
        print("lock1 acquired");
 
        print("executing second operation.");
 
        lock1.unlock();
        lock2.unlock();
    }
 
    // helper methods 
}

咱們運行一下這個例子看下輸出:設計

Thread T1: lock1 acquired, waiting to acquire lock2.
Thread T2: lock2 acquired, waiting to acquire lock1.

一運行這個例子咱們就能看到程序致使了一個死鎖且永遠也退出不了。輸出日誌展現了線程T1在等待lock2,但lock2被線程T2所持有。類似的,線程T2在等待lock1,他被T1所持有。日誌

2.3 避免死鎖

死鎖在Java中是個很常見的併發問題,由於咱們應該設計一個程序來避免潛在的死鎖條件。code

  • 首先咱們應該避免一個線程獲取多個鎖。
  • 其次若是一個線程真的須要多個鎖,咱們應該確保全部線程都以相同的順序獲取鎖,來避免獲取鎖時的循環依賴問題
  • 咱們也可使用帶有超時功能的鎖,像Lock接口中的tryLock方法,來確保一個線程若是獲取不到鎖不會一直阻塞。

三、活鎖

3.1 什麼是活鎖

活鎖是另外一個併發問題,它和死鎖很類似。在活鎖中,兩個或多個線程彼此間一直在轉移狀態,而不像咱們上個例子中互相等待。結果就是全部線程都不能執行它們各自的任務。

一個比較好的活鎖例子就是消息隊列。當發生異常的時候,消息消費者回滾事務並把消息放到隊列頭中,而後相同的消息又從隊列頭中被讀到,又會形成異常並再次放入到隊列頭中。如此循壞往復,消費者永遠讀不到隊列中其餘的消息。

3.2 活鎖舉例

如今咱們展現一下活鎖的狀況,咱們一樣拿上面死鎖的例子來解釋。線程T1調用operation1,線程T2調用operation2,可是咱們稍微改變的操做的邏輯。

兩個線程都須要拿到兩把鎖來完成工做,每一個線程拿到第一個鎖後都會發現拿不到第二把鎖,所以爲了讓另外一個線程先完成任務,每一個線程都會釋放第一把鎖並會嘗試再次獲取到兩把鎖。

咱們來看下下面的測試例子

public class LivelockExample {
 
    private Lock lock1 = new ReentrantLock(true);
    private Lock lock2 = new ReentrantLock(true);
 
    public static void main(String[] args) {
        LivelockExample livelock = new LivelockExample();
        new Thread(livelock::operation1, "T1").start();
        new Thread(livelock::operation2, "T2").start();
    }
 
    public void operation1() {
        while (true) {
            tryLock(lock1, 50);
            print("lock1 acquired, trying to acquire lock2.");
            sleep(50);
 
            if (tryLock(lock2)) {
                print("lock2 acquired.");
            } else {
                print("cannot acquire lock2, releasing lock1.");
                lock1.unlock();
                continue;
            }
 
            print("executing first operation.");
            break;
        }
        lock2.unlock();
        lock1.unlock();
    }
 
    public void operation2() {
        while (true) {
            tryLock(lock2, 50);
            print("lock2 acquired, trying to acquire lock1.");
            sleep(50);
 
            if (tryLock(lock1)) {
                print("lock1 acquired.");
            } else {
                print("cannot acquire lock1, releasing lock2.");
                lock2.unlock();
                continue;
            }
 
            print("executing second operation.");
            break;
        }
        lock1.unlock();
        lock2.unlock();
    }
 
    // helper methods
}

咱們看下運行結果:

Thread T1: lock1 acquired, trying to acquire lock2.
Thread T2: lock2 acquired, trying to acquire lock1.
Thread T1: cannot acquire lock2, releasing lock1.
Thread T2: cannot acquire lock1, releasing lock2.
Thread T2: lock2 acquired, trying to acquire lock1.
Thread T1: lock1 acquired, trying to acquire lock2.
Thread T1: cannot acquire lock2, releasing lock1.
Thread T1: lock1 acquired, trying to acquire lock2.
Thread T2: cannot acquire lock1, releasing lock2.

能看到輸出結果裏,兩個線程都在重複的獲取鎖和釋放鎖,致使兩個線程都不能完成操做。

3.3 避免活鎖

避免活鎖咱們得觀察一下活鎖發生的條件並根據狀況提出方案,好比:

  • 若是咱們有兩個線程在重複的獲取鎖和釋放鎖致使了活鎖,咱們能夠修改下代碼讓兩個線程以一個隨機的時間間隔來獲取鎖,這樣線程就有機會獲取到它們須要的鎖了。
  • 另外一個方式來解決咱們前面提到的消息隊列的問題就是把失敗的消息放到單獨的的隊列中去進一步處理而不是再次放入原隊列中。(這個在實際開發中還真遇到過,開發的時候漏掉了一個狀況,致使循環消費多條錯誤消息,隊列消息大量積壓,要不是隊列報警,險些形成線上bug,因此生產環境的隊列消費最好仍是設置個失敗次數加上死信隊列,否則出問題可真受不了。
相關文章
相關標籤/搜索