多線程之死鎖就是這麼簡單

前言

只有光頭才能變強html

回顧前面:編程

本篇主要是講解死鎖,這是我在多線程的最後一篇了。主要將多線程的基礎過一遍,之後有機會再繼續深刻微信

死鎖是在多線程中也是比較重要的知識點了!多線程

那麼接下來就開始吧,若是文章有錯誤的地方請你們多多包涵,不吝在評論區指正哦~併發

聲明:本文使用JDK1.8ide

1、死鎖講解

在Java中使用多線程,就會有可能致使死鎖問題。死鎖會讓程序一直住,再也不程序往下執行。咱們只能經過停止並重啓的方式來讓程序從新執行。工具

  • 這是咱們很是不肯意看到的一種現象,咱們要儘量避免死鎖的狀況發生!

形成死鎖的緣由能夠歸納成三句話:oop

  • 當前線程擁有其餘線程須要的資源
  • 當前線程等待其餘線程已擁有的資源
  • 都不放棄本身擁有的資源

1.1鎖順序死鎖

首先咱們來看一下最簡單的死鎖(鎖順序死鎖)是怎麼樣發生的:post

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

    public void leftRight() {
		// 獲得left鎖
        synchronized (left) {
			// 獲得right鎖
            synchronized (right) {
                doSomething();
            }
        }
    }

    public void rightLeft() {
		// 獲得right鎖
        synchronized (right) {
			// 獲得left鎖
            synchronized (left) {
                doSomethingElse();
            }
        }
    }
}

咱們的線程是交錯執行的,那麼就頗有可能出現如下的狀況:學習

  • 線程A調用leftRight()方法,獲得left鎖
  • 同時線程B調用rightLeft()方法,獲得right鎖
  • 線程A和線程B都繼續執行,此時線程A須要right鎖才能繼續往下執行。此時線程B須要left鎖才能繼續往下執行。
  • 可是:線程A的left鎖並無釋放,線程B的right鎖也沒有釋放
  • 因此他們都只能等待,而這種等待是無期限的-->永久等待-->死鎖

 

 

1.2動態鎖順序死鎖

咱們看一下下面的例子,你認爲會發生死鎖嗎?

// 轉帳
    public static void transferMoney(Account fromAccount,
                                     Account toAccount,
                                     DollarAmount amount)
            throws InsufficientFundsException {

        // 鎖定匯帳帳戶
        synchronized (fromAccount) {
            // 鎖定來帳帳戶
            synchronized (toAccount) {

                // 判餘額是否大於0
                if (fromAccount.getBalance().compareTo(amount) < 0) {
                    throw new InsufficientFundsException();
                } else {

                    // 匯帳帳戶減錢
                    fromAccount.debit(amount);

                    // 來帳帳戶增錢
                    toAccount.credit(amount);
                }
            }
        }
    }

上面的代碼看起來是沒有問題的:鎖定兩個帳戶來判斷餘額是否充足才進行轉帳!

可是,一樣有可能會發生死鎖

  • 若是兩個線程同時調用transferMoney()
  • 線程A從X帳戶向Y帳戶轉帳
  • 線程B從帳戶Y向帳戶X轉帳
  • 那麼就會發生死鎖。
A:transferMoney(myAccount,yourAccount,10);


B:transferMoney(yourAccount,myAccount,20);

1.3協做對象之間發生死鎖

咱們來看一下下面的例子:

public class CooperatingDeadlock {
    // Warning: deadlock-prone!
    class Taxi {
        @GuardedBy("this") private Point location, destination;
        private final Dispatcher dispatcher;

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

        public synchronized Point getLocation() {
            return location;
        }

        // setLocation 須要Taxi內置鎖
        public synchronized void setLocation(Point location) {
            this.location = location;
            if (location.equals(destination))
                // 調用notifyAvailable()須要Dispatcher內置鎖
                dispatcher.notifyAvailable(this);
        }

        public synchronized Point getDestination() {
            return destination;
        }

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

    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);
        }

        // 調用getImage()須要Dispatcher內置鎖
        public synchronized Image getImage() {
            Image image = new Image();
            for (Taxi t : taxis)
                // 調用getLocation()須要Taxi內置鎖
                image.drawMarker(t.getLocation());
            return image;
        }
    }

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

上面的getImage()setLocation(Point location)都須要獲取兩個鎖的

  • 而且在操做途中是沒有釋放鎖的

這就是隱式獲取兩個鎖(對象之間協做)..

這種方式也很容易就形成死鎖.....

2、避免死鎖的方法

避免死鎖能夠歸納成三種方法:

  • 固定加鎖的順序(針對鎖順序死鎖)
  • 開放調用(針對對象之間協做形成的死鎖)
  • 使用定時鎖-->tryLock()
    • 若是等待獲取鎖時間超時,則拋出異常而不是一直等待

2.1固定鎖順序避免死鎖

上面transferMoney()發生死鎖的緣由是由於加鎖順序不一致而出現的~

  • 正如書上所說的:若是全部線程以固定的順序來得到鎖,那麼程序中就不會出現鎖順序死鎖問題!

那麼上面的例子咱們就能夠改造成這樣子:

public class InduceLockOrder {

    // 額外的鎖、避免兩個對象hash值相等的狀況(即便不多)
    private static final Object tieLock = new Object();

    public void transferMoney(final Account fromAcct,
                              final Account toAcct,
                              final DollarAmount amount)
            throws InsufficientFundsException {
        class Helper {
            public void transfer() throws InsufficientFundsException {
                if (fromAcct.getBalance().compareTo(amount) < 0)
                    throw new InsufficientFundsException();
                else {
                    fromAcct.debit(amount);
                    toAcct.credit(amount);
                }
            }
        }
        // 獲得鎖的hash值
        int fromHash = System.identityHashCode(fromAcct);
        int toHash = System.identityHashCode(toAcct);

        // 根據hash值來上鎖
        if (fromHash < toHash) {
            synchronized (fromAcct) {
                synchronized (toAcct) {
                    new Helper().transfer();
                }
            }

        } else if (fromHash > toHash) {// 根據hash值來上鎖
            synchronized (toAcct) {
                synchronized (fromAcct) {
                    new Helper().transfer();
                }
            }
        } else {// 額外的鎖、避免兩個對象hash值相等的狀況(即便不多)
            synchronized (tieLock) {
                synchronized (fromAcct) {
                    synchronized (toAcct) {
                        new Helper().transfer();
                    }
                }
            }
        }
    }
}

獲得對應的hash值來固定加鎖的順序,這樣咱們就不會發生死鎖的問題了!

2.2開放調用避免死鎖

在協做對象之間發生死鎖的例子中,主要是由於在調用某個方法時就須要持有鎖,而且在方法內部也調用了其餘帶鎖的方法!

  • 若是在調用某個方法時不須要持有鎖,那麼這種調用被稱爲開放調用

咱們能夠這樣來改造:

  • 同步代碼塊最好僅被用於保護那些涉及共享狀態的操做
class CooperatingNoDeadlock {
    @ThreadSafe
    class Taxi {
        @GuardedBy("this") private Point location, 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;

            // 加Taxi內置鎖
            synchronized (this) {
                this.location = location;
                reachedDestination = location.equals(destination);
            }
            // 執行同步代碼塊後完畢,釋放鎖



            if (reachedDestination)
                // 加Dispatcher內置鎖
                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;

            // Dispatcher內置鎖
            synchronized (this) {
                copy = new HashSet<Taxi>(taxis);
            }
            // 執行同步代碼塊後完畢,釋放鎖

            Image image = new Image();
            for (Taxi t : copy)
                // 加Taix內置鎖
                image.drawMarker(t.getLocation());
            return image;
        }
    }

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

}

使用開放調用是很是好的一種方式,應該儘可能使用它~

2.3使用定時鎖

使用顯式Lock鎖,在獲取鎖時使用tryLock()方法。當等待超過期限的時候,tryLock()不會一直等待,而是返回錯誤信息。

使用tryLock()可以有效避免死鎖問題~~

2.4死鎖檢測

雖然形成死鎖的緣由是由於咱們設計得不夠好,可是可能寫代碼的時候不知道哪裏發生了死鎖。

JDK提供了兩種方式來給咱們檢測:

  • JconsoleJDK自帶的圖形化界面工具,使用JDK給咱們的的工具JConsole
  • Jstack是JDK自帶的命令行工具,主要用於線程Dump分析。

具體可參考:

3、總結

發生死鎖的緣由主要因爲:

  • 線程之間交錯執行
    • 解決:以固定的順序加鎖
  • 執行某方法時就須要持有鎖,且不釋放
    • 解決:縮減同步代碼塊範圍,最好僅操做共享變量時才加鎖
  • 永久等待
    • 解決:使用tryLock()定時鎖,超過期限則返回錯誤信息

在操做系統層面上看待死鎖問題(這是我以前作的筆記、很淺顯):

參考資料:

  • 《Java核心技術卷一》
  • 《Java併發編程實戰》
  • 《計算機操做系統 湯小丹》

若是文章有錯的地方歡迎指正,你們互相交流。習慣在微信看技術文章,想要獲取更多的Java資源的同窗,能夠關注微信公衆號:Java3y

文章的目錄導航

相關文章
相關標籤/搜索