編寫高質量代碼:改善Java程序的151個建議(第8章:多線程和併發___建議126~128)

建議126:適時選擇不一樣的線程池來實現

  Java的線程池實現從根本上來講只有兩個:ThreadPoolExecutor類和ScheduledThreadPoolExecutor類,這兩個類仍是父子關係,可是Java爲了簡化並行計算,還提供了一個Exceutors的靜態類,它能夠直接生成多種不一樣的線程池執行器,好比單線程執行器、帶緩衝功能的執行器等,但歸根結底仍是使用ThreadPoolExecutor類或ScheduledThreadPoolExecutor類的封裝類。編程

  爲了理解這些執行器,咱們首先來看看ThreadPoolExecutor類,其中它複雜的構造函數能夠很好的理解線程池的做用,代碼以下:  安全

public class ThreadPoolExecutor extends AbstractExecutorService {
    // 最完整的構造函數
    public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
            long keepAliveTime, TimeUnit unit,
            BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
            RejectedExecutionHandler handler) {
        // 檢驗輸入條件
        if (corePoolSize < 0 || maximumPoolSize <= 0
                || maximumPoolSize < corePoolSize || keepAliveTime < 0)
            throw new IllegalArgumentException();
        // 檢驗運行環境
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
}

  這是ThreadPoolExecutor最完整的構造函數,其餘的構造函數都是引用該構造函數實現的,咱們逐步來解釋這些參數的含義。多線程

  1. corePoolSize:最小線程數。線程啓動後,在池中保持線程的最小數量。須要說明的是線程數量是逐步到達corePoolSize值的,例如corePoolSize被設置爲10,而任務數量爲5,則線程池中最多會啓動5個線程,而不是一次性的啓動10個線程。
  2. maximumPoolSize:最大線程數量。這是池中最大能容納的最大線程數量,若是超出,則使用RejectedExecutionHandler 拒絕策略處理。
  3. keepAliveTime:線程最大生命週期。這裏的生命週期有兩個約束條件,一是該參數針對的是超過corePoolSize數量的線程。二是處於非運行狀態的線程。這麼說吧,若是corePoolSize爲10,maximumPoolSize爲20,此時線程池中有15個線程正在運行,一段時間後,其中有3個線程處於等待狀態的時間超過了keepAliveTime指定的時間,則結束這3個線程,此時線程池中還有12個線程正在運行。
  4. unit:時間單位。這是keepAliveTime的時間單位,能夠是納秒、毫秒、秒、分等選項。
  5. workQuene:任務隊列。當線程池中的線程都處於運行狀態,而此時任務數量繼續增長,則須要一個容器來容納這些任務,這就是任務隊列。
  6. threadFactory:線程工廠。定義如何啓動一個線程,能夠設置線程名稱,而且能夠確認是不是後臺線程等。
  7. handler:拒絕任務處理器。因爲超出線程數量和隊列容量而對繼續增長的任務進行處理的程序。

  線程池的管理是這樣一個過程:首先建立線程池,而後根據任務的數量逐步將線程增大到corePoolSize數量,若是此時仍有任務增長,則放置到workQuene中,直到workQuene爆滿爲止,而後繼續增長池中的數量(加強處理能力),最終達到maximumPoolSize,那若是此時還有任務增長進來呢?這就須要handler處理了,或者丟棄任務,或者拒絕新任務,或者擠佔已有任務等。併發

  在任務隊列和線程池都飽和的狀況下,一但有線程處於等待(任務處理完畢,沒有新任務增長)狀態的時間超過keepAliveTime,則該線程終止,也就說池中的線程數量會逐漸下降,直至爲corePoolSize數量爲止。app

  咱們能夠把線程池想象爲這樣一個場景:在一個生產線上,車間規定是能夠有corePoolSize數量的工人,可是生產線剛創建時,工做很少,不須要那麼多的人。隨着工做數量的增長,工人數量也逐漸增長,直至增長到corePoolSize數量爲止。此時還有任務增長怎麼辦呢?異步

  好辦,任務排隊,corePoolSize數量的工人不停歇的處理任務,新增長的任務按照必定的規則存放在倉庫中(也就是咱們的workQuene中),一旦任務增長的速度超過了工人處理的能力,也就是說倉庫爆滿時,車間就會繼續招聘工人(也就是擴大線程數),直至工人數量到達maximumPoolSize爲止,那若是全部的maximumPoolSize工人都在處理任務時,並且倉庫也是飽和狀態,新增任務該怎麼處理呢?這就會扔一個叫handler的專門機構去處理了,它要麼丟棄這些新增的任務,要麼無視,要麼替換掉別的任務。ide

  過了一段時間後,任務的數量逐漸減小,致使一部分工人處於待工狀態,爲了減小開支(Java是爲了減小系統的資源消耗),因而開始辭退工人,直至保持corePoolSize數量的工人爲止,此時即便沒有工做,也再也不辭退工人(池中的線程數量再也不減小),這也是保證之後再有任務時可以快速的處理。函數

  明白了線程池的概念,咱們再來看看Executors提供的幾個線程建立線程池的便捷方法:性能

  • newSingleThreadExecutor:單線程池。顧名思義就是一個池中只有一個線程在運行,該線程永不超時,並且因爲是一個線程,當有多個任務須要處理時,會將它們放置到一個無界阻塞隊列中逐個處理,它的實現代碼以下:  
 public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

  它的使用方法也很簡單,下面是簡單的示例:測試

public static void main(String[] args) throws ExecutionException,
            InterruptedException {
        // 建立單線程執行器
        ExecutorService es = Executors.newSingleThreadExecutor();
        // 執行一個任務
        Future<String> future = es.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return "";
            }
        });
        // 得到任務執行後的返回值
        System.out.println("返回值:" + future.get());
        // 關閉執行器
        es.shutdown();
    }
  • newCachedThreadPool:緩衝功能的線程。創建了一個線程池,並且線程數量是沒有限制的(固然,不能超過Integer的最大值),新增一個任務即有一個線程處理,或者複用以前空閒的線程,或者重親啓動一個線程,可是一旦一個線程在60秒內一直處於等待狀態時(也就是一分鐘無事可作),則會被終止,其源碼以下: 
  public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

  這裏須要說明的是,任務隊列使用了同步阻塞隊列,這意味着向隊列中加入一個元素,便可喚醒一個線程(新建立的線程或複用空閒線程來處理),這種隊列已經沒有隊列深度的概念了.

  • newFixedThreadPool:固定線程數量的線程池。 在初始化時已經決定了線程的最大數量,若任務添加的能力超出了線程的處理能力,則創建阻塞隊列容納多餘的任務,其源碼以下: 
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

  上面返回的是一個ThreadPoolExecutor,它的corePoolSize和maximumPoolSize是相等的,也就是說,最大線程數量爲nThreads。若是任務增加的速度很是快,超過了LinkedBlockingQuene的最大容量(Integer的最大值),那此時會如何處理呢?會按照ThreadPoolExecutor默認的拒絕策略(默認是DiscardPolicy,直接丟棄)來處理。

  以上三種線程池執行器都是ThreadPoolExecutor的簡化版,目的是幫助開發人員屏蔽過得線程細節,簡化多線程開發。當須要運行異步任務時,能夠直接經過Executors得到一個線程池,而後運行任務,不須要關注ThreadPoolExecutor的一系列參數是什麼含義。固然,有時候這三個線程不能知足要求,此時則能夠直接操做ThreadPoolExecutor來實現複雜的多線程計算。能夠這樣比喻,newSingleThreadExecutor、newCachedThreadPool、newFixedThreadPool是線程池的簡化版,而ThreadPoolExecutor則是旗艦版___簡化版容易操做,須要瞭解的知識相對少些,方便使用,而旗艦版功能齊全,適用面廣,難以駕馭。

建議127:Lock與synchronized是不同的

  不少編碼者都會說,Lock類和synchronized關鍵字用在代碼塊的併發性和內存上時語義是同樣的,都是保持代碼塊同時只有一個線程執行權。這樣的說法只說對了一半,咱們以一個任務提交給多個線程爲例,來看看使用顯示鎖(Lock類)和內部鎖(synchronized關鍵字)有什麼不一樣,首先定義一個任務:

class Task {
    public void doSomething() {
        try {
            // 每一個線程等待2秒鐘,注意此時線程的狀態轉變爲Warning狀態
            Thread.sleep(2000);
        } catch (Exception e) {
            // 異常處理
        }
        StringBuffer sb = new StringBuffer();
        // 線程名稱
        sb.append("線程名稱:" + Thread.currentThread().getName());
        // 運行時間戳
        sb.append(",執行時間: " + Calendar.getInstance().get(Calendar.SECOND) + "s");
        System.out.println(sb);
    }
}

  該類模擬了一個執行時間比較長的計算,注意這裏是模擬方式,在使用sleep方法時線程的狀態會從運行狀態轉變爲等待狀態。該任務具有多線程能力時必須實現Runnable接口,咱們分別創建兩種不一樣的實現機制,先看顯示鎖實現:  

class TaskWithLock extends Task implements Runnable {
    // 聲明顯示鎖
    private final Lock lock = new ReentrantLock();
    @Override
    public void run() {
        try {
            // 開始鎖定
            lock.lock();
            doSomething();

        } finally {
            // 釋放鎖
            lock.unlock();
        }
    }
}

  這裏有一點須要說明,顯示鎖的鎖定和釋放必須放在一個try......finally塊中,這是爲了確保即便出現異常也能正常釋放鎖,保證其它線程能順利執行。

  內部鎖的處理也很是簡單,代碼以下: 

//內部鎖任務
class TaskWithSync extends Task implements Runnable{
    @Override
    public void run() {
        //內部鎖
        synchronized("A"){
            doSomething();
        }
    }
    
}

  這兩個任務看着很是類似,應該可以產生相同的結果吧?咱們創建一個模擬場景,保證同時有三個線程在運行,代碼以下: 

public class Client127 {
    public static void main(String[] args) throws Exception {
        // 運行顯示任務
        runTasks(TaskWithLock.class);
        // 運行內部鎖任務
        runTasks(TaskWithSync.class);
    }

    public static void runTasks(Class<? extends Runnable> clz) throws Exception {
        ExecutorService es = Executors.newCachedThreadPool();
        System.out.println("***開始執行 " + clz.getSimpleName() + " 任務***");
        // 啓動3個線程
        for (int i = 0; i < 3; i++) {
            es.submit(clz.newInstance());
        }
        // 等待足夠長的時間,而後關閉執行器
        TimeUnit.SECONDS.sleep(10);
        System.out.println("---" + clz.getSimpleName() + "  任務執行完畢---\n");
        // 關閉執行器
        es.shutdown();
    }
}

  按照通常的理解,Lock和synchronized的處理方式是相同的,輸出應該沒有差異,可是很遺憾的是,輸出差異其實很大。輸出以下:

        ***開始執行 TaskWithLock 任務***
          線程名稱:pool-1-thread-2,執行時間: 55s
          線程名稱:pool-1-thread-1,執行時間: 55s
          線程名稱:pool-1-thread-3,執行時間: 55s
        ---TaskWithLock  任務執行完畢---

        ***開始執行 TaskWithSync 任務***
          線程名稱:pool-2-thread-1,執行時間: 5s
          線程名稱:pool-2-thread-3,執行時間: 7s
          線程名稱:pool-2-thread-2,執行時間: 9s
        ---TaskWithSync  任務執行完畢---

   注意看運行的時間戳,顯示鎖是同時運行的,很顯然pool-1-thread-1線程執行到sleep時,其它兩個線程也會運行到這裏,一塊兒等待,而後一塊兒輸出,這還具備線程互斥的概念嗎?

  而內部鎖的輸出則是咱們預期的結果,pool-2-thread-1線程在運行時其它線程處於等待狀態,pool-2-threda-1執行完畢後,JVM從等待線程池中隨機獲的一個線程pool-2-thread-3執行,最後執行pool-2-thread-2,這正是咱們但願的。

  如今問題來了:Lock鎖爲何不出現互斥狀況呢?

  這是由於對於同步資源來講(示例中的代碼塊)顯示鎖是對象級別的鎖,而內部鎖是類級別的鎖,也就說說Lock鎖是跟隨對象的,synchronized鎖是跟隨類的,更簡單的說把Lock定義爲多線程類的私有屬性是起不到資源互斥做用的,除非是把Lock定義爲全部線程的共享變量。都說代碼是最好的解釋語言,咱們來看一個Lock鎖資源的代碼:  

public static void main(String[] args) {
        // 多個線程共享鎖
        final Lock lock = new ReentrantLock();
        // 啓動三個線程
        for (int i = 0; i < 3; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        lock.lock();
                        // 休眠2秒鐘
                        Thread.sleep(2000);
                        System.out.println(Thread.currentThread().getName());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        lock.unlock();
                    }
                }
            }).start();
        }
    }

  執行時,會發現線程名稱Thread-0、Thread-一、Thread-2會逐漸輸出,也就是一個線程在執行時,其它線程就處於等待狀態。注意,這裏三個線程運行的實例對象是同一個類。

  除了這一點不一樣以外,顯示鎖和內部鎖還有什麼區別呢?還有如下4點不一樣:

  1. Lock支持更細精度的鎖控制:假設讀寫鎖分離,寫操做時不容許有讀寫操做存在,而讀操做時讀寫能夠併發執行,這一點內部鎖就很難實現。顯示鎖的示例代碼以下:  
class Foo {
    // 可重入的讀寫鎖
    private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    // 讀鎖
    private final Lock r = rwl.readLock();
    // 寫鎖
    private final Lock w = rwl.writeLock();

    // 多操做,可併發執行
    public void read() {
        try {
            r.lock();
            Thread.sleep(1000);
            System.out.println("read......");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            r.unlock();
        }
    }

    // 寫操做,同時只容許一個寫操做
    public void write() {
        try {
            w.lock();
            Thread.sleep(1000);
            System.out.println("write.....");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            w.unlock();
        }
    }

}

  能夠編寫一個Runnable實現類,把Foo類做爲資源進行調用(注意多線程是共享這個資源的),而後就會發現這樣的現象:讀寫鎖容許同時有多個讀操做但只容許一個寫操做,也就是當有一個寫線程在執行時,全部的讀線程都會阻塞,直到寫線程釋放鎖資源爲止,而讀鎖則能夠有多個線程同時執行。

 2.Lock鎖是無阻塞鎖,synchronized是阻塞鎖

    當線程A持有鎖時,線程B也指望得到鎖,此時,若是程序中使用的顯示鎖,則B線程爲等待狀態(在一般的描述中,也認爲此線程被阻塞了),若使用的是內部鎖則爲阻塞狀態。

 3.Lock可實現公平鎖,synchronized只能是非公平鎖

  什麼叫非公平鎖呢?當一個線程A持有鎖,而線程B、C處於阻塞(或等待)狀態時,若線程A釋放鎖,JVM將從線程B、C中隨機選擇一個持有鎖並使其得到執行權,這叫非公平鎖(由於它拋棄了先來後到的順序);若JVM選擇了等待時間最長的一個線程持有鎖,則爲公平鎖(保證每一個線程的等待時間均衡)。須要注意的是,即便是公平鎖,JVM也沒法準確作到" 公平 ",在程序中不能以此做爲精確計算。

  顯示鎖默認是非公平鎖,但能夠在構造函數中加入參數爲true來聲明出公平鎖,而synchronized實現的是非公平鎖,他不能實現公平鎖。

 4.Lock是代碼級的,synchronized是JVM級的

    Lock是經過編碼實現的,synchronized是在運行期由JVM釋放的,相對來講synchronized的優化可能性高,畢竟是在最核心的部分支持的,Lock的優化須要用戶自行考慮。

    顯示鎖和內部鎖的功能各不相同,在性能上也稍有差異,但隨着JDK的不斷推動,相對來講,顯示鎖使用起來更加便利和強大,在實際開發中選擇哪一種類型的鎖就須要根據實際狀況考慮了:靈活、強大選擇lock,快捷、安全選擇synchronized.

建議128:預防線程死鎖

 線程死鎖(DeadLock)是多線程編碼中最頭疼的問題,也是最難重現的問題,由於Java是單進程的多線程語言,一旦線程死鎖,則很難經過外科手術的方法使其起死回生,不少時候只有藉助外部進程重啓應用才能解決問題,咱們看看下面的多線程代碼是否會產生死鎖:

class Foo implements Runnable {
    @Override
    public void run() {
    fun(10);
    }
    // 遞歸方法
    public synchronized void fun(int i) {
        if (--i > 0) {
            for (int j = 0; j < i; j++) {
                System.out.print("*");
            }
            System.out.println(i);
            fun(i);
        }
    }
}

  注意fun方法是一個遞歸函數,並且還加上了synchronized關鍵字,它保證同時只有一個線程可以執行,想一想synchronized關鍵字的做用:當一個帶有synchronized關鍵字的方法在執行時,其餘synchronized方法會被阻塞,由於線程持有該對象的鎖,好比有這樣的代碼:  

class Foo1 {
    public synchronized void m1() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // 異常處理
        }
        System.out.println("m1方法執行完畢");
    }

    public synchronized void m2() {
        System.out.println("m2方法執行完畢");
    }
}

  相信你們都明白,先輸出"m1執行完畢",而後再輸出"m2"執行完畢,由於m1方法在執行時,線程t持有foo對象的鎖,要想主線程得到m2方法的執行權限就必須等待m1方法執行完畢,也就是釋放當前鎖。明白了這個問題,咱們思考一下上例中帶有synchronized的遞歸方法是否能執行?會不會產生死鎖?運行結果以下:

  *********9
  ********8
  *******7
  ******6
  *****5
  ****4
  ***3
  **2
  *1

 一個倒三角形,沒有產生死鎖,正常執行,這是爲什麼呢?很奇怪,是嗎?那是由於在運行時當前線程(Thread-0)得到了Foo對象的鎖(synchronized雖然是標註在方法上的,但實際做用是整個對象),也就是該線程持有了foo對象的鎖,因此它能夠屢次重如fun方法,也就是遞歸了。能夠這樣來思考該問題,一個包廂有N把鑰匙,分別由N個海盜持有 (也就是咱們Java的線程了),可是同一時間只能由一把鑰匙打開寶箱,獲取寶物,只有在上一個海盜關閉了包廂(釋放鎖)後,其它海盜才能繼續打開獲取寶物,這裏還有一個規則:一旦一個海盜打開了寶箱,則該寶箱內的全部寶物對他來講都是開放的,即便是「 寶箱中的寶箱」(即內箱)對他也是開放的。能夠用以下代碼來表示:  

class Foo2 implements Runnable{

    @Override
    public void run() {
        method1();
    }
    public synchronized void method1(){
        method2();
    }
    public synchronized void method2(){
        //doSomething
    }
}

  方法method1synchronized修飾的,方法method2也是synchronized修飾的,method1和method2方法重入徹底是可行的,此種狀況下會不會產生死鎖。

  那什麼狀況下回產生死鎖呢?看以下代碼: 

class A {
    public synchronized void a1(B b) {
        String name = Thread.currentThread().getName();
        System.out.println(name + "  進入A.a1()");
        try {
            // 休眠一秒 仍持有鎖
            Thread.sleep(1000);
        } catch (Exception e) {
            // 異常處理
        }
        System.out.println(name + "  試圖訪問B.b2()");
        b.b2();
    }

    public synchronized void a2() {
        System.out.println("進入a.a2()");
    }
}

class B {
    public synchronized void b1(A a) {
        String name = Thread.currentThread().getName();
        System.out.println(name + "  進入B.b1()");
        try {
            // 休眠一秒 仍持有鎖
            Thread.sleep(1000);
        } catch (Exception e) {
            // 異常處理
        }
        System.out.println(name + "  試圖訪問A.a2()");
        a.a2();
    }

    public synchronized void b2() {
        System.out.println("進入B.b2()");
    }
}
public static void main(String[] args) throws InterruptedException {
        final A a = new A();
        final B b = new B();
        // 線程A
        new Thread(new Runnable() {
            @Override
            public void run() {
                a.a1(b);
            }
        }, "線程A").start();
        // 線程B
        new Thread(new Runnable() {
            @Override
            public void run() {
                b.b1(a);
            }
        }, "線程B").start();
    }

  此段程序定義了兩個資源A和B,而後在兩個線程A、B中使用了該資源,因爲兩個資源之間交互操做,而且都是同步方法,所以在線程A休眠一秒鐘後,它會試圖訪問資源B的b2方法。可是B線程持有該類的鎖,並同時在等待A線程釋放其鎖資源,因此此時就出現了兩個線程在互相等待釋放資源的狀況,也就是死鎖了,運行結果以下:

   線程A  進入A.a1()
      線程B  進入B.b1()
  線程A  試圖訪問B.b2()
  線程B  試圖訪問A.a2()

  此種狀況下,線程A和線程B會一直等下去,直到有外界干擾爲止,好比終止一個線程,或者某一線程自行放棄資源的爭搶,不然這兩個線程就始終處於死鎖狀態了。咱們知道達到線程死鎖須要四個條件:

  1. 互斥條件:一個資源每次只能被一個線程使用
  2. 資源獨佔條件:一個線程因請求資源在未使用完以前,不能強行剝奪
  3. 不剝奪條件:線程已經得到的資源在未使用完以前,不能強行剝奪
  4. 循環等待條件:若干線程之間造成一種頭尾相接的循環等待資源關係

  只有知足了這些條件才能產生線程死鎖,這也同時告誡咱們若是要解決線程死鎖問題,就必須從這四個條件入手,通常狀況下能夠按照如下兩種方案解決:

  (1)、避免或減小資源共享

    一個資源被多個線程共享,若採用了同步機制,則產生死鎖的可能性大,特別是在項目比較龐大的狀況下,很難杜絕死鎖,對此最好的解決辦法就是減小資源共享。

    例如一個B/S結構的辦公系統能夠徹底忽略資源共享,這是由於此類系統有三個特徵:一是併發訪問不會過高,二是讀操做多於寫操做,三是數據質量要求比較低,所以即便出現數據資源不一樣步的狀況也不可能產生太大影響,徹底能夠不使用同步技術。可是若是是一個支付清算系統就必須慎重考慮資源同步問題了,由於此係統一是數據質量要求很是高(若是產生數據不一樣步的狀況那但是重大生產事故),二是併發量大,不設置數據同步則會產生很是多的運算邏輯失效的狀況,這會致使交易失敗,產生大量的"髒數據",系統可靠性大大下降。

  (2)、使用自旋鎖

    回到前面的例子,線程A在等待線程B釋放資源,而線程B又在等待線程A釋放資源,僵持不下,那若是線程B設置了超時時間是否是就能夠解決該死鎖問題了呢?好比線程B在等待2秒後仍是沒法得到資源,則自行終結該任務,代碼以下:   

public void b2() {
        try {
            // 馬上得到鎖,或者2秒等待鎖資源
            if (lock.tryLock(2, TimeUnit.SECONDS)) {
                System.out.println("進入B.b2()");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

  上面的代碼中使用tryLock實現了自旋鎖(Spin Lock),它跟互斥鎖同樣,若是一個執行單元要想訪問被自旋鎖保護的共享資源,則必須先獲得鎖,在訪問完共享資源後,也必須釋放鎖。若是在獲取自旋鎖時,沒有任何執行單元保持該鎖,那麼將當即獲得鎖;若是在獲取自旋鎖時已經有保持者,那麼獲取鎖操做將"自旋" 在哪裏,直到該自旋鎖的保持者釋放了鎖爲止,在咱們的例子中就是線程A等待線程B釋放鎖,在2秒內  不斷嘗試是否可以得到鎖,達到2秒後還未得到鎖資源,線程A則結束運行,線程B將得到資源繼續執行,死鎖解除。

  對於死鎖的描述最經典的案例是哲學家進餐(五位哲學家圍坐在圓形餐桌旁,人手一根筷子,作一下兩件事情:吃飯和思考。要求吃東西的時候中止思考,思考的時候中止吃東西,並且必須使用兩根筷子才能吃東西),解決此問題的方法不少,好比引入服務生(資源地調度)、資源分級等方法均可以很好的解決此類死鎖問題。在咱們Java多線程併發編程中,死鎖很難避免,也不容易預防,對付它的最好方法就是測試:提升測試覆蓋率,創建有效的邊界測試,增強資源監控,這些方法能使得死鎖無可遁形,即便發生了死鎖現象也能迅速查到緣由,提升系統性能。

相關文章
相關標籤/搜索