1. 程式人生 > >Java併發程式設計實戰--協作物件間的死鎖與開放呼叫

Java併發程式設計實戰--協作物件間的死鎖與開放呼叫

某些獲取多個鎖的操作並不像在LeftRightDeadlock或transferMoney中那麼明顯,這兩個鎖並不一定必須在同一個方法中被獲取。下面兩個相互協作的類,在出租車排程系統中可能會找到它們。Taxi代表一個計程車物件,包含位置和目的地兩個屬性,Dispatcher代表一個計程車車隊。

public class Taxi {
    private final Dispatcher dispatcher;
    private Point location, destination;

    public Taxi(Dispatcher dispatcher) {
        this
.dispatcher = dispatcher; } public synchronized Point getLocation() { return location; } public synchronized void setLocation(Point location){ this.location = location; if(location.equals(destination)){ dispatcher.notifyAvaliable(this); } } } public
class Dispatcher { private final Set<Taxi> taxis; private final Set<Taxi> avaliableTaxis; public Dispatcher() { taxis = new HashSet<Taxi>(); avaliableTaxis = new HashSet<Taxi>(); } public synchronized void notifyAvaliable(Taxi taxi) { avaliableTaxis.add(taxi); } public
synchronized Image getImage() { Image image = new Image(); for (Taxi t : taxis) { image.drawMarker(t.getLocation()); } return image; } }

儘管沒有任何方法會顯式地獲取兩個鎖,但setLocation和getImage等方法的呼叫者都會獲得兩個鎖。如果一個執行緒在收到GPS接收器的更新事件時呼叫setLocation,那麼它將首先更新出租車的位置,然後判斷它是否到達了目的地。如果已經到達,它會通知Dispatcher:它需要一個新的目的地。因為setLocation和notifyAvailable都是同步方法,因此呼叫setLocation的執行緒將首先獲取Taxi的鎖,然後獲取Dispatcher的鎖。同樣,呼叫getImage的執行緒將首先獲取Dispatcher的鎖,然後再獲取每一個Taxi的鎖(每次獲取一個)。這與LeftRightDeadlock中的情況相同,兩個執行緒按照不同的順序來獲取兩個鎖,因此就可能產生死鎖。

在LeftRightDeadlock或transferMoney中,要查詢死鎖是比較簡單的,只需要找出那些需要獲取兩個鎖的方法。然而要在Taxi和Dispatcher中查詢死鎖則比較困難:如果在持有鎖的情況下呼叫某個外部方法,那麼就需要警惕死鎖。

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

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

如果在呼叫某個方法時不需要持有鎖,那麼這種呼叫被稱為開放呼叫。依賴於開放呼叫的類通常能表現出更好的行為,並且在與那些在呼叫方法時需要持有鎖的類相比,也更易於編寫。通過儘可能地使用開放呼叫,將更易於找出那些需要獲取多個鎖的程式碼路徑,因此也就更容易確保採用一致的順序來獲得鎖。

開放呼叫需要使程式碼塊僅被用於保護那些涉及共享狀態的操作,如下程式所示,如果只是為了語法緊湊或簡單性(而不是因為整個方法必須通過一個鎖來保護)而使用同步方法(而不是同步程式碼塊):

@ThreadSafe
class Taxi {
    @GuardedBy("this")
    private Point location;
    @GuardedBy("this")
    private Point destination;
    private final Dispatcher dispatcher;

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

    public synchronized Point getLocation() {
        return location;
    }

    public synchronized void setLocation(Point location) {
        boolean reachedDestination;

        synchronized (this) {
            this.location = location;
            reachedDestination = location.equals(destination);
        }

        if (reachedDestination) {
            dispatcher.notifyAvailable(this);
        }
    }

    public synchronized Point getDestination() {
        return destination;
    }

    public synchronized void setDestination(Point destination) {
        this.destination = destination;
    }
}


@ThreadSafe
class Dispatcher {
    @GuardedBy("this")
    private final Set<Taxi> taxis;
    @GuardedBy("this")
    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 Image getImage() {
        Set<Taxi> copy;

        synchronized (this) {
            copy = new HashSet<Taxi>(taxis);
        }

        Image image = new Image();

        for (Taxi t : copy)
            image.drawMarker(t.getLocation());

        return image;
    }
}


class Image {
    public void drawMarker(Point p) {
    }
}

有時候,在重新編寫同步程式碼塊以使用開發呼叫時會產生意想不到的結果,因為這會使得某個原子操作變為非原子操作。在許多情況下,使某個操作失去原子性是可以接受的。例如,對於兩個操作:更新出租車位置以及通知排程程式這輛計程車已準備好出發去一個新的目的地,這兩個操作並不需要實現為一個原子操作。在其他情況下,雖然去掉原子性可能會出現一些值得注意的結果,但這種語義變化仍然是可以接受的。在容易產生死鎖的版本中,getImage會生成某個時刻下的整個車隊位置的完整快照,而在重新改寫的版本中,getImage將獲得每輛計程車不同時刻的位置。

然而,在某些情況下,丟失原子性會引發錯誤,此時需要通過另一種技術來實現原子性。例如,在構建一個併發物件時,使得每次只有單個執行緒執行使用了開放呼叫的程式碼路徑。例如,在關閉某個服務時,你可能希望所有正在執行的操作執行完成以後,再釋放這些服務佔用的資源。如果在等待操作完成的同時持有該服務的鎖,那麼將很容易導致死鎖,但如果在服務關閉之前就釋放服務的鎖,則可能導致其他執行緒開始新的操作。這個問題的解決方法是,在將服務的狀態更新為”關閉“之前一直持有鎖,這樣其他想要開始新的操作的執行緒,包括想關閉該服務的其他執行緒,會發現服務已經不可用,因此也就不會試圖開始新的操作。然後,你可以等待關閉操作結束,並且知道當開放呼叫完成後,只有執行關閉操作的執行緒才能訪問該服務的狀態。因此,這項技術依賴於構造的一些協議(而不是通過加鎖)來防止其他執行緒進入程式碼的臨界區。