1. 程式人生 > >Java高併發程式設計:活躍性危險

Java高併發程式設計:活躍性危險

Java高併發程式中,不得不出現資源競爭以及一些其他嚴重的問題,比如死鎖、執行緒飢餓、響應性問題和活鎖問題。在安全性與活躍性之間通常存在依賴,我們使用加鎖機制來確保執行緒安全,但是如果過度地使用加鎖,則可能導致鎖順序死鎖(Lock-Ordering DeadLock)。

1.死鎖

死鎖定義:當一個執行緒永遠地持有一個鎖,並且其他執行緒都嘗試獲取這個鎖的時候,那麼它們將永遠被阻塞。線上程A持有L鎖並且想獲得M鎖的同時,執行緒B持有M鎖並且想獲得L鎖的時候,那麼這兩個先將永遠等待下去,這就是最簡單的死鎖形式。(抱死鎖)其中多個執行緒由於存在環路的鎖依賴關係而永遠地等待下去。

JVM在解決死鎖的問題方面沒有資料庫服務那麼強大。當一組執行緒發生死鎖的時候,”遊戲“到此結束————這些執行緒永遠不能再使用了。根據執行緒完成的工作不同,可能造成應用程式完全停止,或者某個特定的子系統停止,或者是效能降低。恢復應用程式的唯一方式就是終止並且重啟它,並希望不要再發生同樣的事情。

1.1 鎖順序問題

在這裡插入圖片描述

public class LeftRightDeadLock {
    private final Object left=new Object();
    private final Object right=new Object();

    private
CountDownLatch countDownLatch=new CountDownLatch(1); public void leftRight(){ try { countDownLatch.await(); synchronized (left){ Thread.sleep(1000); synchronized (right){ System.out.println("-------leftRight---------"
); } } } catch (InterruptedException e) { e.printStackTrace(); } } public void rightLeft(){ try { countDownLatch.await(); synchronized (right){ Thread.sleep(1000); synchronized (left){ System.out.println("-------Rightleft---------"); } } } catch (InterruptedException e) { e.printStackTrace(); } } public void countDown(){ countDownLatch.countDown(); } public static void main(String[] args) { LeftRightDeadLock leftRightDeadLock=new LeftRightDeadLock(); new Thread(new Runnable() { @Override public void run() { leftRightDeadLock.leftRight(); } }).start(); new Thread(new Runnable() { @Override public void run() { leftRightDeadLock.rightLeft(); } }).start(); leftRightDeadLock.countDown(); } }

在LeftRightDeadLock中發生死鎖的原因是:兩個執行緒以不同的順序來獲得相同的鎖。如果按照相同的順序來請求鎖,那麼久不會出現迴圈的加鎖依賴性,因此也就不會產生死鎖了。

1.2 動態順序死鎖

有時候我們並不清楚地知道是否在鎖順序上有足夠的控制權來避免死鎖。

執行緒不安全
public class TransferMoney {
    public void transferMoney(Account fromAccount, Account toAccount){
        synchronized (fromAccount){
            synchronized (toAccount){
                //do transferMoney
                System.out.println("--------transferMoney--------");
            }
        }
    }
}

解決方案:我們可以通過調整鎖順序來避免死鎖。比如下面:

public class ThreadSafeTransferMoney {
    public static final Object tileLock=new Object();
    
    public void transferMoney(Account fromAccount, Account toAccount){
        int fromHash=System.identityHashCode(fromAccount);
        int toHash=System.identityHashCode(toAccount);
        
        if(fromHash > toHash){
            synchronized (fromAccount){
                synchronized (toAccount){
                    //do transferMoney
                    System.out.println("--------transferMoney--------");
                }
            }
        }else if(fromHash < toHash){
            synchronized (fromAccount){
                synchronized (toAccount){
                    //do transferMoney
                    System.out.println("--------transferMoney--------");
                }
            }
        }else {
            synchronized (tileLock) {
                synchronized (fromAccount) {
                    synchronized (toAccount) {
                        //do transferMoney
                        System.out.println("--------transferMoney--------");
                    }
                }
            }
        }
    }
}

1.3 在協作物件之間發生死鎖

public class Point {
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public Point() {
    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }

    @Override
    public String toString() {
        return "Point{" +
                "x=" + x +
                ", y=" + y +
                '}';
    }
}

public class Taxi {
    private final Dispatcher dispatcher;

    private Point location;

    private Point destination;

    public Taxi(Dispatcher dispatcher) {
        this.dispatcher = dispatcher;
    }

    public synchronized Point getLocation(){
        return location;
    }

    private synchronized void setLocation(Point location){
        this.location=location;
        if(location.equals(destination)){
            dispatcher.notifyAvaiable(this);
        }
    }
}

public class Dispatcher {
    private final Set<Taxi> taxis;
    private final Set<Taxi> availableTaxis;

    public Dispatcher() {
        taxis=new HashSet<>();
        availableTaxis=new HashSet<>();
    }

    public synchronized void notifyAvaiable(Taxi taxi){
        availableTaxis.add(taxi);
    }

    public synchronized List<Point> getImage(){
        List<Point> list=new ArrayList<>();
        for (Taxi taxi : taxis) {
            list.add(taxi.getLocation());
        }
        return list;
    }
}

Taxi呼叫setLocation方法需要先獲取Taxi的鎖,然後再獲得Dispatcher的鎖。Dispatcher的getImage方法需要先獲取Dispatcher的鎖,然後再獲取Taxi的鎖,這與LeftRightDeadLock中的情況相同,兩個執行緒按照不同的順序獲取鎖,因此可能造成死鎖。

如果在持有鎖的情況下呼叫某個外部方法,那麼將出現活躍性問題。在這個外部方法中可能會獲取其他的鎖(這可能造成死鎖),或者阻塞時間過長,導致其他執行緒無法及時獲得當前被持有的鎖。為了解決這個協同物件之間的呼叫死鎖問題,下面我們就將介紹開發呼叫方法。

1.4 開放呼叫

在Taxi和Dispatcher中並不知道它們要陷入死鎖,況且本來它們就不應該知道。方法呼叫相當於一種抽象屏障,因而你無須瞭解在被呼叫方法中所執行的操作。但也正是由於不知道在被呼叫方法中執行的操作,因此在持有鎖的時候對呼叫某個外部方法難以進行分析,所以可能會導致出現死鎖。

總結:如果我們能夠在呼叫方法的時候不需要持有鎖,那麼這種呼叫就是一種開放呼叫。依賴於開放呼叫的類通常能表現出更好的行為,並且與那些在呼叫方法時需要持有鎖的類相比,也更容易編寫。

public class Taxi {
    private final Dispatcher dispatcher;

    private Point location;

    private Point destination;

    public Taxi(Dispatcher dispatcher) {
        this.dispatcher = dispatcher;
    }

    public synchronized Point getLocation(){
        return location;
    }

    private void setLocation(Point location){
        boolean readchedDestination=false;

        synchronized (this){
            this.location=location;
            if(location.equals(destination)){
                readchedDestination=true;
            }
        }

        if (readchedDestination)
            dispatcher.notifyAvaiable(this);
    }
}

public class Dispatcher {
    private final Set<Taxi> taxis;
    private final Set<Taxi> availableTaxis;

    public Dispatcher() {
        taxis=new HashSet<>();
        availableTaxis=new HashSet<>();
    }

    public synchronized void notifyAvaiable(Taxi taxi){
        availableTaxis.add(taxi);
    }

    public List<Point> getImage(){
        List<Point> list=new ArrayList<>();
        Set<Taxi> copy=new HashSet<>();

        synchronized (this){
            copy.addAll(taxis);
        }

        for (Taxi taxi : copy) {
            list.add(taxi.getLocation());
        }
        return list;
    }
}

1.5 資源死鎖

定義:正當多個執行緒相互持有彼此正在等待的鎖而又不釋放自己已持有的鎖時會發生死鎖,當他們相同的資源集合等待時,也會發生死鎖。

如果一個任務需要連線兩個資料庫,並且在請求這兩個資源時不會始終遵循相同的順序,那麼執行緒A可能持有與資料庫D1的連線,並等待與資料庫D2的連線,而執行緒B則持有與D2的連線並等待與D1的連線(資源池越大,出現這種情況的可能性就越小,如果每個資源池都有N個連線,那麼在發生死鎖時不僅需要N個迴圈等待的執行緒,而且還需要大量不恰當的執行時序)

另一個資源的死鎖形式就是執行緒飢餓死鎖(Thread-Starvation Deadlock)。 比如個任務提交另一個任務,並等待被提交任務在單執行緒的Executor中執行完成。這種情況天,第一個任務將永遠等待下去,並使得另一個任務以及在這個Executor中執行的所有其他任務都停止執行。

2.死鎖的避免與診斷

如果必須獲取多個鎖,那麼在設計時必須考慮鎖的順序:儘量減少潛在的加鎖 互動數量,將獲取鎖時需要遵循的協議寫入正式文件並始終遵循這些協議。

死鎖避免地方法:

  1. 首先,找出在什麼地方將獲取多個鎖(使這個集合儘量小)
  2. 然後對所有這些例項進行全域性分析,從而確保它們在整個程式中獲取鎖的順序都保持一致。
  3. 儘可能地使用開放呼叫,這能極大地簡化分析過程。

如果所有的呼叫都是開放呼叫,那麼要發現獲取多個鎖的例項是非常簡單的,可以通過程式碼審查或者藉助自動化的原始碼分析工具。

2.1 支援定時的鎖(Timed Lock Attempts)

當使用內建鎖時,只要沒有獲得鎖,就會永遠等待下去,而顯式鎖則可以執行一個超時時限(Timeout),在等待超過該事件後tryLock會返回一個失敗資訊。

即使在整個系統中沒有始終使用定時鎖,使用定時鎖來獲取多個鎖也能有效地應對死鎖問題。 如果在獲取鎖時超時,那麼可以釋放這個鎖,然後後退並在一段時間後再次蠶食,從而消除了死鎖發生的條件,使程式恢復過來。(這項技術只有在同時獲取兩個鎖時才有效,如果在巢狀的方法呼叫中請求多個鎖,那麼即使你知道已經有了外層的鎖,也無法釋放它)

2.2 通過執行緒轉儲資訊來分析死鎖(Deadlock Analysis with Thread Dumps)

JVM通過執行緒轉儲(Thread Dump)來幫助識別死鎖的發生。 執行緒轉儲包括各個執行中的執行緒的棧追蹤資訊,這類似於發生異常時的棧追蹤資訊。 執行緒轉儲還包含加鎖資訊,例如每個執行緒持有了哪些鎖,在那些棧幀中獲得這些鎖,以及被阻塞的執行緒正在等待獲取哪一個鎖。

在生成執行緒轉儲之前,JVM將在等待關係圖中通過搜尋迴圈來找出死鎖。如果發現了一個死鎖,則獲取相應的死鎖資訊,例如在死鎖中涉及哪些鎖和執行緒,以及這個鎖的獲取操作位於程式的哪些位置.

3. 活躍性危險

死鎖時最常見的活躍性危險,在併發執行緒中還存在一些其他的活躍性危險,包括:飢餓,丟失訊號和活鎖等。

3.1 飢餓(Starvation)

當執行緒由於無法訪問它所需要的資源而不能繼續執行時,就發生了“飢餓(Starvation)”。

引發飢餓的最常見資源就是CPU時鐘週期。如果在Java應用程式中對執行緒的優先順序使用不當,或者在持有鎖時執行一些無法結束的結構(例如無限迴圈,或無限制等待某個資源),那麼也可能導致飢餓,因為其他需要這個鎖的執行緒將無法得到它。

在Thread API定義的執行緒優先順序只是作為執行緒排程的參考。在Thread API中定義了10個優先順序,JVM根據需要將它們對映到作業系統的排程優先順序,這種對映時與特定平臺(不同的作業系統)相關的。在某些作業系統中,如果優先順序的數量少於10個,那麼有多個Java優先順序會被對映到同一個優先順序。

要避免使用執行緒優先順序,因為這會增加平臺依賴性,並可能導致活躍性問題。在大多數併發應用程式中,都可以使用預設的執行緒優先順序。

3.2 糟糕的響應性(Poor Responsiveness)

CPU密集型的後臺任務仍可能對響應性造成影響,因為它們會與事件執行緒共同競爭CPU的時鐘週期。

解決方案:如果由其他執行緒完成的工作都是後臺任務,那麼應該降低它們的優先順序,從而提高前臺程式的響應性。

3.3 活鎖

活鎖定義:活鎖(Livelock)時另一種形式的活躍性問題,儘管不會阻塞執行緒,但也不能繼續執行,因為執行緒不斷重複執行相同的操作,而且總會失敗。

  1. 活鎖通常發生在處理事務訊息的應用程式中:如果不能成功地處理某個訊息,那麼訊息處理機制將回滾整個事務,並將它重新放到佇列的開頭。如果訊息處理其在處理某種特定型別的訊息時存在錯誤並導致它失敗,那麼每當這個訊息從佇列中取出並傳遞到存在錯誤的處理器時,都會發生事務回滾。由於這條訊息又被放回到佇列開頭,因此處理器將被反覆呼叫,並返回先溝通的結果(有時候也被稱為毒藥訊息,Poison Message)。這種形式的活鎖通常時由過度的錯誤恢復程式碼造成的,因為它錯誤將不可修復的錯誤作為可修復的錯誤。

  2. 當多個相互協作的執行緒都對彼此進行響應從而修改各自的狀態,並使得任何一個執行緒都無法繼續執行時,就發生了活鎖。 這就像兩個過於禮貌的人在半路上面對面相遇了:他們彼此都讓出對方的路,然後又在另一條路上相遇了,因此他們就這樣反覆地避讓下去。

解決方案:要解決這種活鎖問題,需要在重試機制中引入隨機性(randomness)。為了避免這種情況發生,需要讓它們分別等待一段隨機的時間

在併發應用程式中,通過等待隨機長度的時間和回退可以有效地避免活鎖的發生。