1. 程式人生 > >多線程之死鎖就是這麽簡單

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

釋放 哪裏 子類 private 同時 ava AI 共享 strong

前言

只有光頭才能變強

回顧前面:

  • ThreadLocal就是這麽簡單
  • 多線程三分鐘就可以入個門了!
  • 多線程基礎必要知識點!看了學習多線程事半功倍
  • Java鎖機制了解一下
  • AQS簡簡單單過一遍
  • Lock鎖子類了解一下
  • 線程池你真不來了解一下嗎?

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

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

那麽接下來就開始吧,如果文章有錯誤的地方請大家多多包涵,不吝在評論區指正哦~

聲明:本文使用JDK1.8

一、死鎖講解

在Java中使用多線程,就會有可能導致死鎖問題。死鎖會讓程序一直住,不再程序往下執行。我們只能通過中止並重啟

的方式來讓程序重新執行。

  • 這是我們非常不願意看到的一種現象,我們要盡可能避免死鎖的情況發生!

造成死鎖的原因可以概括成三句話:

  • 當前線程擁有其他線程需要的資源
  • 當前線程等待其他線程已擁有的資源
  • 都不放棄自己擁有的資源

1.1鎖順序死鎖

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


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)都需要獲取兩個鎖的

  • 並且在操作途中是沒有釋放鎖的

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

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

二、避免死鎖的方法

避免死鎖可以概括成三種方法:

  • 固定加鎖的順序(針對鎖順序死鎖)
  • 開放調用(針對對象之間協作造成的死鎖)
  • 使用定時鎖-->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分析。

具體可參考:

  • https://www.cnblogs.com/flyingeagle/articles/6853167.html

三、總結

發生死鎖的原因主要由於:

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

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

  • 操作系統第五篇【死鎖】

參考資料:

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

如果文章有錯的地方歡迎指正,大家互相交流。習慣在微信看技術文章,想要獲取更多的Java資源的同學,可以關註微信公眾號:Java3y。為了大家方便,剛新建了一下qq群:742919422,大家也可以去交流交流。謝謝支持了!希望能多介紹給其他有需要的朋友

文章的目錄導航

  • https://zhongfucheng.bitcron.com/post/shou-ji/wen-zhang-dao-hang

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