1. 程式人生 > >《JAVA併發程式設計實戰》避免活躍性危險

《JAVA併發程式設計實戰》避免活躍性危險

文章目錄


在安全性和活躍性之間通常存在著某種制衡。我們使用加鎖機制確保執行緒安全,但如果過度使用加鎖,則可能導致鎖順序死鎖。同樣,我們使用執行緒池和訊號量來限制對資源的使用,但這些被限制的行為可能會導致資源死鎖。

死鎖

其中多個執行緒由於存在環路的鎖依賴關係而永遠的等待下去,這種情況是最簡單的死鎖形式(抱死Deadly Embrace)。

鎖順序死鎖

兩個執行緒試圖以不同順序來獲得相同的鎖。
在這裡插入圖片描述

//容易發生死鎖
public class LeftRightDeadLock{
    private final Object left = new Object();
    private final Object right = new Object();
    
    public void leftRight(){
        synchronized(left){
            synchronized(right){
                doSomething();
            }
} } public void rightLeft(){ synchronized(right){ synchronized(left){ doSomethingElse(); } } } }

動態的鎖順序死鎖

//注意容易發生死鎖
public void transferMoney(Account from,Account to,DollarAmount amount) throws InsufficientFundsException{
synchronized(from){ synchronzied(to){ if(from.getBalance().compareTo(amount) < 0){ throw new InsufficientFundsException(); } else { from.debit(amount); to.credit(amount); } } } }

在制定鎖順序時,可以使用System.identityHashCode方法,該方法將返回由Object.hashCode返回的值。

private static final Object tieLock = new Object();

public void transferMoney(final Account from,final Account to,final DollarAmount amout) throws InsufficientFundsException{
    class Helper{
        public void transfer() throws InsufficientFundsException{
            if(from.getBalance().compareTo(amount) < 0){
                throw new InsufficientFundsException();
            } else {
                from.debit(amount);
                to.credit(amount);
            }
        }
    }
    
    int fromHash = System.identityHashCode(from);
    int toHash = System.indentityHashCode(to);
    
    if(fromHash < toHash){
        synchronized(from){
            synchronized(to){
                new Helper().transfer();
            }
        }
    } else if(fromHash > toHash){
        synchronized(to){
            synchronized(fo){
                new Helper().transfer();
            }
        }    
    } else {
        synchronized(tieLock) {
            synchronized(from){
                synchronized(to){
                    new Helper().transfer();
                }
            }
        }
    }
}

在極少數情況下,兩個物件可能擁有相同的雜湊值,此時必須通過某種任意方法來決定鎖的順序,而這可能又會重新引入死鎖。為了避免這種情況,可以使用“加時賽(Tie Breaking)”鎖。在獲得兩個Account鎖之前,首先獲得這個“加時賽”鎖,從而保證每次只有一個執行緒以未知的順序獲得這兩個鎖,從而消除了死鎖的可能性。

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

Taxi表示一個計程車物件,包含位置和目的地兩個屬性,Dispatcher代表一個計程車車隊。

class Taxi{
    private Point location,destination;
    private final Dispatcher dispatcher;
    
    public Taxi(Dispatcher dispatcher){
        this.dispatcher = dispatcher;
    }
    
    public synchronized Point getLocation(){
        return location;
    }
    
    public synchronzied void setLocation(Point location){
        this.location = location;
        if(location.equals(destination){
            dispathcer.notifyAvailable(this);
        }
    }
}

public class Dispatcher{
    private final Set<Taxi> taxis;
    private final Set<Taxi> availableTaxis;
    
    public Dispatcher(){
        taxis = new HashSet<Taxi>();
        availableTaxis = new HashSet<Taxi>();
    }
    
    public synchronized void notifyAvailable(Taxi taxi){
        availableTaxis.add(taxi);
    }
    
    public synchronzied void Image getImage(){
        Image image = new Image();
        for(Taxi t : taxis){
            image.drawMarker(t.getLocation());
        }
        return image;
    }
}

在呼叫setLocation時,會先持有Taxi的鎖,然後再獲取Dispatcher鎖。而,呼叫getLocation,會先獲取Dispatcher鎖,然後再獲取Taxi的鎖。

如果在持有鎖時呼叫某個外部方法,那麼將出現活躍性問題。在這個外部方法中可能會獲取其他鎖(著可能產生死鎖),或者阻塞時間過長,導致其他執行緒無法及時獲得當前被持有的鎖。

開放呼叫

如果在呼叫某個方法時不需要持有鎖,那麼這種呼叫被稱為開放呼叫(Open Call)

class Taxi{
    private Point location,destination;
    private final Dispatcher dispatcher;
    
    public Taxi(Dispatcher dispatcher){
        this.dispatcher = dispatcher;
    }
    
    public synchronized Point getLocation(){
        return location;
    }
    
    public void setLocation(Point location){
        boolean reachedDestination;
        synchronized(this){
            this.location = location;
            reachedDestination = location.equals(destination);
        }
        if(reachedDestination){
            dispathcer.notifyAvailable(this);   
        }
    }
}

public class Dispatcher{
    private final Set<Taxi> taxis;
    private final Set<Taxi> availableTaxis;
    
    public Dispatcher(){
        taxis = new HashSet<Taxi>();
        availableTaxis = new HashSet<Taxi>();
    }
    
    public synchronized void notifyAvailable(Taxi taxi){
        availableTaxis.add(taxi);
    }
    
    public synchronzied void Image getImage(){
        Set<Taxi> copy;
        synchronized(this){
            copy = new HashSet<>(taxis);
        }
        Image image = new Image();
        for(Taxi t : copy){
            image.drawMarker(t.getLocation());
        }
        return image;
    }
}

資源死鎖

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

有界執行緒池/資源池和相互依賴的任務不能一起使用

死鎖的避免和診斷

如果一個程式每次至多隻能獲得一個鎖,那麼就不會產生鎖順序死鎖.如果必須獲取多個鎖,那麼在設計時必須考慮鎖的順序:儘量減少潛在的加鎖互動數量,將獲取鎖時需要遵循的協議寫入文件並始終遵循這些協議。

在使用細粒度鎖的程式程式中,可以通過使用一種兩階段策略來檢查程式碼中的死鎖:首先,找出在什麼地方將獲取多個鎖,然後對所有這些例項進行全域性分析,從而確保他們在整個程式中獲取鎖的順序都是保持一致的。儘可能的使用開放呼叫。

支援定時的鎖

當使用內建鎖時,只要沒有獲得鎖,就會永遠等待下去,而顯式鎖則可以指定一個超時時限,在迭代超過該時間後tryLock會返回一個失敗資訊。

當定時鎖失敗時,你能記錄發生的失敗和相關資訊,並通過一種更平緩的方式來重新啟動計算,而不是關閉整個程序。

使用執行緒轉儲資訊來分析死鎖

JVM使用執行緒轉儲(Thread Dump)來幫助識別死鎖的發生。

執行緒轉儲包括各個執行中的執行緒的棧追蹤資訊、加鎖資訊等,例如每個執行緒持有了哪些鎖,在哪些棧幀中獲得這些鎖,已經被阻塞的執行緒正在等待獲取哪一個鎖。

在UNIX平臺下觸發執行緒轉儲操作,可以通過向JVM程序傳送SIGQUIT訊號(kill -3).或者在UNIX平臺中按下CTRL+,在Windows平臺中按下CTRL+BREAK(用筆記本的同學,方法是按下“Ctrl+Fn+b”組合鍵;),IDE中大多都可以請求執行緒轉儲。

Java stack information for the threads listed above:
===================================================
"Thread-1":
	at Thread_learning.DeadThread$2.run(DeadThread.java:35)
	- waiting to lock <0x00000000d5f05cd0> (a java.lang.Object)
	- locked <0x00000000d5f05ce0> (a java.lang.Object)
	at java.lang.Thread.run(Unknown Source)
"Thread-0":
	at Thread_learning.DeadThread$1.run(DeadThread.java:25)
	- waiting to lock <0x00000000d5f05ce0> (a java.lang.Object)
	- locked <0x00000000d5f05cd0> (a java.lang.Object)
	at java.lang.Thread.run(Unknown Source)

Found 1 deadlock.

其他活躍性危險

飢餓

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

程式在一些奇怪的地方呼叫Thread.sleep或Thread.yield,這是因為該程式試圖克服優先順序調整問題或響應性問題,並試圖讓低優先順序的執行緒執行更多時間。

要避免使用執行緒優先順序,因為這會增加平臺依賴性,並可能導致活躍性問題。

活鎖

活鎖不會阻塞執行緒,但也不能繼續執行,因為執行緒將不斷重複執行系統的操作,而且總會失敗。活鎖通常發生在處理事務訊息的應用程式中:如果不能成功的處理某個訊息,那麼訊息處理機制將回滾整個事務,並將它重新放到佇列的頭部。處理器反覆呼叫佇列頭部任務,又將反覆失敗。

當多個相互協作的執行緒都對彼此進行響應從而修改各自的狀態,並使得任何一個執行緒都無法繼續執行時,就發生了活鎖。

要解決活鎖問題,通常在重試機制中引入隨機性。