Java併發編程-各類鎖

安全性和活躍度一般相互牽制。咱們使用鎖來保證線程安全,可是濫用鎖可能引發鎖順序死鎖。相似地,咱們使用線程池和信號量來約束資源的使用,redis

可是缺不能知曉哪些管轄範圍內的活動可能造成的資源死鎖。Java應用程序不能從死鎖中恢復,因此確保你的設計可以避免死鎖出現的先決條件是很是有價值。算法

一.死鎖sql

經典的「哲學家進餐」問題很好的闡釋了死鎖。5個哲學家一塊兒出門去吃中餐,他們圍坐在一個圓桌邊。他們只有五隻筷子(不是5雙),每兩我的中間放有一隻。數據庫

哲學家邊吃邊思考,交替進行。每一個人都須要得到兩隻筷子才能吃東西,可是吃後要把筷子放回原處繼續思考。有一些管理筷子的算法,使每個人都可以或多或少,及時安全

吃到東西(一個飢餓的哲學家試圖得到兩隻臨近的筷子,可是若是其中的一隻正在被別人佔用,那麼他英愛放棄其中一隻可用的筷子,等待幾分鐘再嘗試)。可是這樣作可能致使數據結構

一些哲學家或者全部哲學家都餓死 (每一個人都迅速捉住本身左邊的筷子,而後等待本身右邊的筷子變成可用,同時並不放下左邊的筷子)。這最後一種狀況,當每一個人都擁有他人須要的多線程

資源,而且等待其餘人正在佔有的資源,若是你們一致佔有資源,直到得到本身須要卻沒佔有的其餘資源,若是你們一致佔有資源,直到得到本身須要卻沒被佔有的其餘資源,那麼就會產生死鎖。併發

  當一個線程永遠佔有一個鎖,而其餘線程嘗試去得到這個鎖,那麼他們將永遠被阻塞。當線程Thread1佔有鎖A時,想要得到鎖B,可是同時線程Thread2持有B鎖,並嘗試得到A鎖,兩個線程將永遠等待下去。分佈式

這種狀況是死鎖最簡單的形式.ide

例子以下代碼:

public class DeadLock {

    private static Object lockA = new Object();

    private static Object lockB = new Object();

    public static void main(String[] args) {
        new DeadLock().deadLock();
    }

    private void deadLock() {

        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                synchronized (lockA){
                    try {
                        System.out.println(Thread.currentThread().getName() + "獲取A鎖 ing!");
                        Thread.sleep(500);
                        System.out.println(Thread.currentThread().getName() + "睡眠500ms");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "須要B鎖!!!");
                    synchronized (lockB){
                        System.out.println(Thread.currentThread().getName() + "B鎖獲取成功");
                    }
                }
            }
        },"Thread1");

        Thread thread2 = new Thread(new Runnable() {
            public void run() {
                synchronized (lockB){
                    try {
                        System.out.println(Thread.currentThread().getName() + "獲取B鎖 ing!");
                        Thread.sleep(500);
                        System.out.println(Thread.currentThread().getName() + "睡眠500ms");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "須要A鎖!!!");
                    synchronized (lockA){
                        System.out.println(Thread.currentThread().getName() + "A鎖獲取成功");
                    }
                }
            }
        },"Thread2");

        thread1.start();
        thread2.start();

    }

}

運行結果以下圖:

結果很明顯了,這兩個線程陷入了死鎖狀態了,發生死鎖的緣由是,兩個線程試圖經過不一樣的順序得到多個相同的鎖。若是請求鎖的順序相同,

就不會出現循環的鎖依賴現象(你等我放鎖,我等你放鎖),也就不會產生死鎖了。若是你可以保證同時請求鎖A和鎖B的每個線程,都是按照從鎖A到鎖B的順序,那麼就不會發生死鎖了。

若是全部線程以通用的固定秩序獲取鎖,程序就不會出現鎖順序死鎖問題了。

 

什麼狀況下會發生死鎖呢?

  1.鎖的嵌套容易發生死鎖。解決辦法:獲取鎖時,查看是否有嵌套。儘可能不要用鎖的嵌套,若是必需要用到鎖的嵌套,就要指定鎖的順序,由於參數的順序是超乎咱們控制的,爲了解決這個問題,咱們必須指定鎖的順序,而且在整個應用程序中,

得到鎖都必須始終遵照這個既定的順序。

 

上面的例子出現死鎖的根本緣由就是獲取所的順序是亂序的,超乎咱們控制的。上面例子最理想的狀況就是把業務邏輯抽離出來,把獲取鎖的代碼放在一個公共的方法裏面,讓這兩個線程獲取鎖

都是從個人公共的方法裏面獲取,當Thread1線程進入公共方法時,獲取了A鎖,另外Thread2又進來了,可是A鎖已經被Thread1線程獲取了,Thread1接着又獲取鎖B,Thread2線程就不能再獲取不到了鎖A,更別說再去獲取鎖B了,這樣就有必定的順序了。

上面例子的改造以下:

public class DeadLock {

    private static Object lockA = new Object();

    private static Object lockB = new Object();

    public static void main(String[] args) {
        new DeadLock().deadLock();
    }

    private void deadLock() {

        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                getLock();
            }
        },"Thread1");

        Thread thread2 = new Thread(new Runnable() {
            public void run() {
                getLock();
            }
        },"Thread2");

        thread1.start();
        thread2.start();

    }

    public void getLock() {
        synchronized (lockA){
            try {
                System.out.println(Thread.currentThread().getName() + "獲取A鎖 ing!");
                Thread.sleep(500);
                System.out.println(Thread.currentThread().getName() + "睡眠500ms");
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "須要B鎖!!!");
            synchronized (lockB){
                System.out.println(Thread.currentThread().getName() + "B鎖獲取成功");
            }
        }
    }

}

運行結果以下:

能夠看到把業務邏輯抽離出來,把獲取鎖的代碼放在一個公共的方法裏面,得到鎖都必須始終遵照這個既定的順序。

 

2.引入顯式鎖的超時機制特性來避免死鎖

超時機制是監控死鎖和從死鎖中恢復的技術,是使用每一個顯式所Lock類中定時tryLock特性,來替代使用顳部所機制。在內部鎖的機制中,只要沒有得到鎖,就永遠保持等待,而

顯示的鎖使你能狗定義超時的時間,在規定時間以後tryLock尚未得到鎖就會返回失敗。經過使用超時,儘管這段時間比你預期可以得到所的時間長不少,你仍然能夠在乎外發生後從新

得到控制權。當嘗試得到定時鎖失敗時,你並不須要知道緣由。也許是由於有死鎖發生,也許是線程在持有鎖的時候錯誤地進入無限循環;也有多是執行一些活動所花費的時間比你

預期慢了許多。不過至少你有機會了解到你的嘗試已經失敗,記錄下此次嘗試中有用的信息,並從新開始計算,這遠比關閉整個線程要優雅得多。

  即便定時鎖並無應用於整個系統,使用它來得到多重鎖仍是可以有效應對死鎖。若是獲取鎖的請求超時,你能夠釋放這個鎖,並後退,等待一會後再嘗試,這極可能消除了死鎖發生的條件,

而且循序程序恢復。(這項技術只有在同時得到兩個鎖的時候纔有效;若是多個鎖是在嵌套的方法中被請求的,你沒法僅僅釋放外層的鎖,儘管你知道本身已經持有該鎖)

 

顯式鎖Lock,Lock是一個接口,定義了一些抽象的所操做。與內部鎖機制不一樣,Lock提供了無條件,可輪詢,定時的,可中斷的鎖獲取操做,全部加鎖和解鎖的方法都是顯式的。

Lock的實現必須提供舉報與內部鎖相同的內存可見性的語義。可是加鎖的語義,調度算法,順序保證,性能特性這些能夠不一樣。

Lock接口源碼以下:

public interface Lock {
    //加鎖
    void lock();

    //可中斷的鎖,打算線程的等待狀態,即A線程已經獲取該鎖,B線程又來獲    
    //取,可是A線程會通知B,來打算B線程的等待。
     void lockInterruptibly() throws InterruptedException;   
     //嘗試去獲取鎖,失敗返回False
     boolean tryLock(); 


    //超時機制獲取鎖
     boolean tryLock(long time, TimeUnit unit) throws 
     InterruptedException;

    //釋放鎖
     void unlock();

     Condition newCondition();

}

 

ReentranLock實現了Lock接口,提供了與synchronized相同的互斥和內存可見性的保證。得到ReentrantLock的鎖與進入synchronized塊有着相同內存含義,釋放ReentrantLock鎖與退出synchronized塊有着相同內存含義。

ReentrantLock提供了與synchronized同樣可重入加鎖的語義。ReentrantLock支持Lock接口定義的全部獲取鎖的方式。與synchronized相比,ReentranLock爲處理不可用的鎖提供了更多靈活性。

可是對於如今的JDK的更新,synchronized的性能被優化的愈來愈好,內部鎖(synchronized)已經得到至關可觀的性能,性能不只僅是個不斷變化的目標,並且變化的很是快。

以下圖:

看到圖,隨着JDK的更新迭代,內部鎖的性能愈來愈快,這不是ReentrantLock的衰退,而是內部鎖(synchronized)愈來愈快,特別在JDK目前跟新到如今1.9.

 

下面用顯式鎖Lock再來改造上面的例子

public class DeadLock {

    Lock lock = new ReentrantLock();

    private static Object lockA = new Object();

    private static Object lockB = new Object();

    public static void main(String[] args) {
        new DeadLock().deadLock();
    }

    private void deadLock() {

        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                try {
                    lock.lock();
                    System.out.println(Thread.currentThread().getName() + "獲取A鎖 ing!");
                    Thread.sleep(500);
                    System.out.println(Thread.currentThread().getName() + "睡眠500ms");
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
                System.out.println(Thread.currentThread().getName() + "須要B鎖!!!");
                try {
                    lock.lock();
                    System.out.println(Thread.currentThread().getName() + "B鎖獲取成功");
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }, "Thread1");

        Thread thread2 = new Thread(new Runnable() {
            public void run() {
                try {
                    lock.lock();
                    System.out.println(Thread.currentThread().getName() + "獲取B鎖 ing!");
                    Thread.sleep(500);
                    System.out.println(Thread.currentThread().getName() + "睡眠500ms");
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
                System.out.println(Thread.currentThread().getName() + "須要A鎖!!!");
                try {
                    lock.lock();
                    System.out.println(Thread.currentThread().getName() + "A鎖獲取成功");
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }, "Thread1");

        thread1.start();
        thread2.start();
    }

}

運行結果以下:

 

能夠看到顯示鎖Lock是能夠避免死鎖的。

 

注意:Lock接口規範形式。這種模式在某種程度上比使用內部鎖更加複雜:鎖必須在finally塊中釋放。另外一方面,若是鎖守護的代碼在try塊以外拋出了異常,它將永遠都不會被釋放了;若是對象

可以被置於不一致狀態,可能須要額外的try-catch,或try-finally塊。(當你在使用任何形式的鎖時,你老是應該關注異常帶來的影響,包括內部鎖)。

忘記時候finally釋放Lock是一個定時炸彈。當不幸發生的時候,你將很難追蹤到錯誤的發生點,由於根本沒有記錄鎖本應該被釋放的位置和時間。這就是ReentrantLock不能徹底替代synchronized的緣由:它更加危險,

由於當程序的控制權離開守護的塊,不會自動清除鎖。儘管記得在finally塊中釋放鎖並不苦難,但忘記的可能仍然存在。

sy

 

可輪詢的和可定時的鎖請求

  可定時的與可輪詢的鎖獲取模式,是由tryLock方法實現,與物體愛建的鎖獲取相比,它具備更完善的錯誤恢復機制。在內部鎖中,死鎖是致命的,惟一的恢復方法是從新啓動程序,惟一的預防方法是在構建程序時不要出錯,

因此不可能循序不一致的鎖順序。可定時的與可輪詢的鎖提供了另一個選擇:能夠規避死鎖的放生。

  若是你不能得到全部須要的鎖,那麼使用可定時的與可輪詢的獲取方式(tryLock)使你可以從新拿到控制權,它會釋放你已經得到的這些鎖,而後再從新嘗試(或者至少會記錄這個失敗,抑或者採起其餘措施)。使用tryLock試圖得到兩個鎖,

若是不能同時得到兩個,就回退,並從新嘗試。休眠時間由一個特定的組件管理,並由一個隨機組件減小活鎖發生的可能性。若是必定時間內,沒有得到全部須要的鎖,就會返回一個失敗狀態,這樣操做就能優雅的失敗了。

tryLock()常常與if esle一塊兒使用。

 

讀-寫鎖

 

  ReentrantLock實現了標準的互斥鎖:一次最多隻有一個線程可以持有相同ReentrantLock。可是互斥一般作爲保護數據一致性的很強的加鎖約束,所以,過度的限制了併發性。互斥是保守的加鎖策略,避免了

「寫/寫」和「寫/讀"的重讀,可是一樣避開了"讀/讀"的重疊。在不少狀況下,數據結構是」頻繁被讀取「的——它們是可變的,有時候會被改變,但多數訪問只進行讀操做。此時,若是可以放寬,容許多個讀者同時訪問數據結構就

很是好了。只要每一個線程保證可以讀到最新的數據(線程的可見性),而且在讀者讀取數據的時候沒有其餘線程修改數據,就不會發生問題。這就是讀-寫鎖容許的狀況:一個資源可以被多個讀者訪問,或者被一個寫者訪問,二者不能同時進行。

ReadWriteLock,暴露了2個Lock對象,一個用來讀,另外一個用來寫。讀取ReadWriteLock鎖守護的數據,你必須首先得到讀取的鎖,當須要修改ReadWriteLock守護的數據,你必須首先得到寫入鎖。

ReadWriteLock源碼接口以下:

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

讀寫鎖實現的加鎖策略容許多個同時存在的讀者,可是隻容許一個寫者。與Lock同樣,ReadWriteLock容許多種實現,形成性能,調度保證,獲取優先,公平性,以及加鎖語義等方面的不盡相同。

讀寫鎖的設計是用來進行性能改進的,使得特定狀況下可以有更好的併發性。時間實踐中,當多處理器系統中,頻繁的訪問主要爲讀取數據結構的時候哦,讀寫鎖可以改進性能;在其餘狀況下運行的狀況比獨佔

的鎖要稍微差一些,這歸因於它更大的複雜性。使用它可否帶來改進,最好經過對系統進行剖析來判斷:好在ReadWriteLock使用Lock做爲讀寫部分的鎖,因此若是剖析得的結果發現讀寫鎖沒有能提升性能,把讀寫鎖置換爲獨佔鎖是比較容易。

 

下面咱們用synchonized來進行讀操做,對於讀操做性能如何呢?

例子以下:

public class ReadWriteLockTest {
    private ReentrantReadWriteLock rw1 = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        final ReadWriteLockTest test = new ReadWriteLockTest();

        new Thread(){
            @Override
            public void run() {
                test.get(Thread.currentThread());
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                test.get(Thread.currentThread());
            }
        }.start();

    }

    public synchronized void get(Thread thread) {

        long start = System.currentTimeMillis();
        while (System.currentTimeMillis() - start <= 1){
            System.out.println(thread.getName() + "正在讀操做");
        }
        System.out.println(thread.getName() + "讀操做完成");

    }

}

運行結果以下:

能夠看到要線程Thread0讀操做完了,Thread1才能進行讀操做。明顯這樣性能很慢。

 

如今咱們用ReadWriteLock來進行讀操做,看一下性能如何

例子以下:

public class ReadWriteLockTest {
    private ReentrantReadWriteLock rw1 = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        final ReadWriteLockTest test = new ReadWriteLockTest();

        new Thread(){
            @Override
            public void run() {
                test.get(Thread.currentThread());
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                test.get(Thread.currentThread());
            }
        }.start();

    }

    public  void get(Thread thread) {
        try {
            rw1.readLock().lock();
            long start = System.currentTimeMillis();
            while (System.currentTimeMillis() - start <= 1){
                System.out.println(thread.getName() + "正在讀操做");
            }
            System.out.println(thread.getName() + "讀操做完成");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rw1.readLock().unlock();
        }

    }

}

運行結果以下:

能夠看到線程間是不用排隊來讀操做的。這樣效率明顯很高。

 

咱們再看一下寫操做,以下:

public class ReadWriteLockTest {
    private ReentrantReadWriteLock rw1 = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        final ReadWriteLockTest test = new ReadWriteLockTest();

        new Thread(){
            @Override
            public void run() {
                test.get(Thread.currentThread());
            }
        }.start();

        new Thread(){
            @Override
            public void run() {
                test.get(Thread.currentThread());
            }
        }.start();

    }

    public  void get(Thread thread) {
        try {
            rw1.writeLock().lock();
            long start = System.currentTimeMillis();
            while (System.currentTimeMillis() - start <= 1){
                System.out.println(thread.getName() + "正在寫操做");
            }
            System.out.println(thread.getName() + "寫操做完成");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rw1.writeLock().unlock();
        }

    }

}

運行結果以下:

能夠看到ReadWriteLock只容許一個寫者。

 

公平鎖

ReentrantReadWriteLock爲兩個鎖提供了可重入的加鎖語義,它是繼承了ReadWriteLock,擴展了ReadWriteLock。它與ReadWriteLock相同,ReentrantReadWriteLock可以被構造

爲非公平鎖(構造方法不設置參數,默認是非公平),或者公平。在公平鎖中,選擇權交給等待時間最長的線程;若是鎖由讀者得到,而一個線程請求寫入鎖,那麼再也不容許讀者得到讀取鎖,直到寫者被受理,平且已經釋放了寫鎖。

在非公平的鎖中,線程容許訪問的順序是不定的。由寫者降級爲讀者是容許的;從讀者升級爲寫者是不容許的(嘗試這樣的行爲會致使死鎖)

  當鎖被持有的時間相對較長,而且大部分操做都不會改變鎖守護的資源,那麼讀寫鎖可以改進併發性。ReadWriteMap使用了ReentrantReadWriteLock來包裝Map,使得它可以在多線程間

被安全的共享,並仍然可以避免 "讀-寫" 或者 」寫-寫「衝突。顯示中ConcurrentHashMap併發容器的性能已經足夠好了,因此你能夠是使用他,而沒必要使用這個新的解決方案,若是你須要併發的部分

只有哈希Map,可是若是你須要爲LinkedHashMap這種可替換元素Map提供更好的併發訪問,那麼這項技術是很是有用的。

用讀寫鎖包裝的Map以下圖:

讀寫鎖的性能以下圖:

 

總結:

  顯式的Lock與內部鎖相比提供了一些擴展的特性,包括處理不可用的鎖時更好的靈活性,以及對隊列行爲更好的控制。可是ReentrantLock不能徹底替代synchronized;只有當你須要

synchronized沒能提供的特性時才應該使用。

  讀-寫鎖容許多個讀者併發訪問被守護的對象,當訪問多爲讀取數據結構的時候,它具備改進可伸縮性的潛力。

 

數據庫層面上的鎖——悲觀鎖和樂觀鎖

樂觀鎖:他對世界比較樂觀,認爲別人訪問正在改變的數據的機率是很低的,因此直到修改完成準備提交所作的的修改到數據庫的時候纔會將數據鎖住。完成更改後釋放。

我想一下一個這樣的業務場景:咱們從數據庫中獲取了一條數據,咱們正要修改他的數據時,恰好另一個用戶此時已經修改過了這條數據,這是咱們是不知作別人修改過這條數據的。

  解決辦法,咱們能夠在表中增長一個version字段,讓這個version自增或者自減,或者用一個時間戳字段,這個時間搓字段是惟一的。咱們寫數據的時候帶上version,也就是每一個人更新的時候都會判斷當前的版本號是否跟我查詢出來獲得的版本號是否一致,不一致就更新失敗,一致就更新這條記錄並更改版本號。

例子以下:

1.查詢出商品信息
select (status,status,version) from t_goods where id=#{id}
2.根據商品信息生成訂單
3.修改商品status爲2
update t_goods 
set status=2,version=version+1
where id=#{id} and version=#{version};

用戶體驗表現層面一般表現爲系統繁忙之類的。

在這裏還要注意樂觀鎖的一個細節:就是version字段要自增或者自減,否者會出現ABA問題

ABA問題:線程Thread1拿到了version字段爲A,因爲CAS操做(即先進行比較而後設值),線程Thread2先拿到的version,將version改爲B,線程Thread3來拿到version,將version值又改回了A。此時Thread1的CAS(先比較後set值)操做結束了,繼續執行,它發現version的值仍是A,覺得沒有發生變化,因此就繼續執行了。這個過程當中,version從A變爲B,再由B變爲A就被形象地稱爲ABA問題了。

 

悲觀鎖也稱排它鎖,當事務在操做數據時把這部分數據進行鎖定,直到操做完畢後再解鎖,其餘事務操做纔可操做該部分數據。這將防止其餘進程讀取或修改表中的數據。

通常使用 select ...for update 對所選擇的數據進行加鎖處理,例如

select * from account where name=」JAVA」 for update,

 

 

 這條sql 語句鎖定了account 表中全部符合檢索條件(name=」JAVA」)的記錄。本次事務提交以前(事務提交時會釋放事務過程當中的鎖),外界沒法修改這些記錄。

用戶界面常表現爲轉圈圈等待。

 

若是數據庫分庫分表了,再也不是單個數據庫了,那麼咱們能夠用分佈式鎖,好比redis的setnx特性,zookeeper的節點惟一性和順序性特性來作分佈式鎖。

相關文章
相關標籤/搜索