小學徒成長系列—線程同步、死鎖、線程池


  在前一篇博文《小學徒的成長系列—線程》中,我們已經講解了關於線程的基本概念及其常用的方法,現在在本次博文中,我們就講解關於守護線程,同步,及線程池的知識吧。

 1.守護線程(后台線程)

  在Java中,線程定義有兩種:

  1> 非守護線程(有些教學書籍喜歡叫做非后台線程)

  2> 守護線程(有些教學書籍喜歡叫做后台線程),下面是摘自《Java編程思想》的說法:

  所謂后台線程,是指在程序運行的時候在后台提供一種通用服務的線程,並且這種線程並不屬於程序中不可或缺的部分,因此,當所有的非后台線程結束時,程序也就終止了,同時會殺死進程中的所有后台線程。反過來說,只要有任何非后台線程還在運行,程序就不會終止。比如,執行main()方法的就是一個后台線程。

  當然,並不是只有由JVM創建的才是守護線程啦,其實我們也可以定義守護線程,通過Thread類的setDaemon()定義即可。

  下面是sun公司提供的JConsole中的截圖

2.線程同步問題

  我們訪問很多網站的時候,往往都會有一個計數器,顯示我們是第幾個訪問該網站的,下面我們來模擬一下,eg:

 1 package com.thread.tongbu;
 2 
 3 public class NumberAddThread implements Runnable {
 4     public static int number = 0;
 5     
 6     @Override
 7     public void run() {
 8         timmer();  //當多個線程訪問該方法修改數據時,將會涉及數據安全問題
 9     }
10     //計算該線程第幾個訪問的
11     public void timmer() {
12         number++;
13      
14         try {
15             //讓該線程睡眠0.1s
16             Thread.sleep(100);    
17         } catch (InterruptedException e) {
18             e.printStackTrace();
19         }
20         //輸出該線程是第幾個訪問該變量的
21         System.out.println(Thread.currentThread().getName() + " : 你是第 " + number + " 個訪問");
22     }
23     
24     public static void main(String[] args) {
25         NumberAddThread n = new NumberAddThread();
26         Thread t1 = new Thread(n);
27         Thread t2 = new Thread(n);
28         t1.start();
29         t2.start();
30     }
31 }

  本來正常的情況下輸出結果應該是 : , 但我們發現結果竟然出乎意料是 :

  這究竟是為什么呢?

  其實原因在於,剛開始,第一個線程訪問的時候,number已經自加為1,然后該線程睡眠了,在它睡眠期間,跌二個線程來了,也給number加1變成了2,這個時候第一個線程才睡眠結束繼續執行下一行輸出語句,然而此時的number的值已經改變了,輸出的結果也不再是1了。換句話說,上面的問題就是run()方法體不具備同步安全性。

  為了解決這個問題,Java的多線程引入了同步監視器來解決這個問題,使用同步監視器的代碼塊就是同步代碼塊,同步代碼塊的格式如下:

  1>修飾對象:

synchronized(obj) {
    //....
    //此處的代碼塊就是同步代碼塊   
}

  2>修飾方法,表示整個方法為同步方法:

public synchronized void timmer() {
      //......
      //此處的代碼塊就是同步代碼塊        

  注意:synchronized只能修飾對象和方法,不能用來修飾構造器、屬性。

  上面代碼塊中,不管synchronized修飾的是方法還是對象,它始終鎖定的是對象的實例變量,或者類變量。當執行同步代碼塊的時候,就會先獲取該對象的同步監視器的鎖定,直到線程執行完同步代碼塊之后才會釋放對同步監視器的鎖定。

  到現在大家應該知道怎么解決前面程序出現的問題了吧,沒錯,只要把run()方法修改成如下即可:

1     public void run() {
2         synchronized(this){
3             timmer();
4         }
5     }

 執行結果:

  啊哈,這下我們終於對啦,呵呵。

   3.死鎖

  3.1基本概念

  3.1.1什么叫做死鎖?

  多個線程在執行過程中因爭奪資源而造成的一種僵局,若無外力作用,將無法向前推進。

  3.1.2產生死鎖的原因

  1> 競爭資源。當系統中供多個線程共享的資源如打印機等,其數目不足以滿足諸線程的需要時,會引起諸線程對資源的競爭而產生死鎖;

  2> 線程間推進順序非法,線程在運行過程中,請求和釋放資源的順序不當,也同樣會導致產生進程死鎖。

  3.1.3產生死鎖的必要條件

  1> 互斥條件,即一段時間某資源只由一個線程占用;

  2> 請求和保持條件,指進程已經保持了至少一個資源,但又提出了新的資源請求新的資源求情,而該資源又已經被其他線程占有,此時請求進程序阻塞,但又對自己已經獲得的其他資源保持不放;

  3> 不剝奪條件,指線程已經獲得的資源,在未使用完之前,不能被剝奪,只能在使用完時自己釋放;

  4> 環路等待,指在發生死鎖時,必然存在一個進程—資源的環形鏈。如P1等待一個P2占用的資源,P2正在等待P3占用的資源,P3正在等待P1占用的資源。

  3.1.4死鎖的解除方式

  1>剝奪資源,從其他進程剝奪足夠數量的資源給死鎖進程,以解除死鎖狀態;

  2>撤銷進程,最簡單的撤銷進程的方法是使全部死鎖進程都夭折掉,稍微溫和一點的方法是按照某種順序逐個撤銷進程,直至有足夠的資源可用,使死鎖狀態消除為止。

 3.2Java程序中的死鎖狀況及其調試方法

  首先我們來看一個程序,eg:

 1 package com.thread.tongbu;
 2 
 3 public class TestDeadLock implements Runnable{
 4     static class Pen {}
 5     static class Paper{}
 6     
 7     boolean flag = false;
 8     static Paper paper = new Paper();
 9     static Pen pen = new Pen();
10     
11     @Override
12     public void run() {
13         if(flag) {
14             synchronized (paper) {
15                 try {
16                     Thread.sleep(100);
17                 } catch (InterruptedException e) {
18                     e.printStackTrace();
19                 }
20                 synchronized (pen) {
21                     System.out.println("paper");
22                 }
23             }
24         } else {
25             synchronized (pen) {
26                 try {
27                     Thread.sleep(100);
28                 } catch (InterruptedException e) {
29                     e.printStackTrace();
30                 }
31                 synchronized (paper) {
32                     System.out.println("pen");
33                 }
34             }
35         }
36     }
37     
38     public static void main(String[] args) {
39         TestDeadLock td1 = new TestDeadLock();
40         TestDeadLock td2 = new TestDeadLock();
41         td1.flag = false;
42         td2.flag = true;
43         Thread tt1 = new Thread(td2);
44         Thread tt2 = new Thread(td1); 
45         tt1.start();
46         tt2.start();
47     }
48 }

  執行的時候,等待了好久,都一直沒有出現輸出結果,線程也一直沒有結束,這是因為出現死鎖了。

  那我們怎么確定一定是死鎖呢?有兩種方法。

  1>使用JDK給我們的的工具JConsole,可以通過打開cmd然后輸入jconsole打開。

    1)連接到需要查看的進程。

2)打開線程選項卡,然后點擊左下角的“檢測死鎖”

    3)jconsole就會給我們檢測出該線程中造成死鎖的線程,點擊選中即可查看詳情:

     從上圖中我們可以看出:

      ①在線程Thread-1中,從狀態可以看出,它想申請Paper這個資源,但是這個資源已經被Thread-0擁有了,所以就堵塞了。

      ②在線程Thread-0中,從狀態可以看出,它想申請Pen這個資源,但是這個資源已經被Thread-1擁有了,所以就堵塞了。

    Thread-1一直等待paper資源,而Thread--一直等待pen資源,於是這兩個線程就這么僵持了下去,造成了死鎖。

  2>直接使用JVM自帶的命令

    1)首先通過 jps 命令查看需要查看的Java進程的vmid,如圖,我們要查看的進程TestDeadLock的vmid號是7412;

    

    2)然后利用 jstack 查看該進程中的堆棧情況,在cmd中輸入 jstack -l 7412 ,移動到輸出的信息的最下面即可得到:

    

    至此,相信大家都會看了吧,具體就不說啦,根據輸出,找到問題所在的代碼,開始調試解決即可啦。

 4.線程池

  4.1簡介

  我們都知道對象的創建和銷毀都是很消耗性能的,所以為了最大程度的復用對象,降低性能的消耗,就出現了容器對象池,而線程池的本質也是對象池,所以線程池能夠最大程度上的復用已有的線程對象,當然除此之外,他還能夠最大程度上的復用線程,否則他就不叫線程池啦。我記得我在面試金山的時候,面試官百分百的肯定線程是不能復用的,我當時就不太贊同,當然我也沒有理論,因為當時的我,在這塊確實不太熟悉,那一次的面試,也第一次讓我意識到了我的基礎還是太薄弱了。好啦,扯淡了,不好意思,我們講講線程池是怎樣復用線程的吧。

  本來線程在執行完畢之后就會被掛載或者銷毀的,但是,不斷的掛載或銷毀,是需要一定開銷的的,但是如果我們讓線程完成任務后忙等待一會兒,就可以維持存在,根據調度策略分配任務給他,就又能復用該線程執行多個任務,減少了線程掛起,恢復,銷毀的開銷,當然啦,如果一直讓線程長期忙等待的話,也是非常消耗性能的。

  下面這個線程類關系圖摘自:www-35java-com的博客

  

  當然啦,實際定義線程池的是ThreadPoolExecutor類,但是Java官網的API強烈推薦我們使用Executors,因為它已經為大多數使用情景預定義了設置:

4.2ThreadPoolExecutor

  在這個類中,有一個關鍵的類Worker,所有的線程對象都要經過Worker的包裝,這樣才能夠做到復用線程而無需創建新的線程,關於這個Worker類我們在以后的博文會介紹到,這次我們只是看看ThreadPoolExecutor類的構造方法並且解析一下吧

 1     public ThreadPoolExecutor(int corePoolSize,
 2                               int maximumPoolSize,
 3                               long keepAliveTime,
 4                               TimeUnit unit,
 5                               BlockingQueue<Runnable> workQueue,
 6                               ThreadFactory threadFactory,
 7                               RejectedExecutionHandler handler) {
 8         if (corePoolSize < 0 ||
 9             maximumPoolSize <= 0 ||
10             maximumPoolSize < corePoolSize ||
11             keepAliveTime < 0)
12             throw new IllegalArgumentException();
13         if (workQueue == null || threadFactory == null || handler == null)
14             throw new NullPointerException();
15         this.corePoolSize = corePoolSize;
16         this.maximumPoolSize = maximumPoolSize;
17         this.workQueue = workQueue;
18         this.keepAliveTime = unit.toNanos(keepAliveTime);
19         this.threadFactory = threadFactory;
20         this.handler = handler;
21     }

 

根據Java官網文檔的解釋,構造方法中每個變量的解釋如下:

1> corePoolSize :線程池維護線程的最小數量,哪怕是空閑的

2>maximumPoolSize : 線程池維護的最大線程數量  
  由於ThreadPoolExecutor 將根據 corePoolSize和 maximumPoolSize設置的邊界自動調整池大小,其調整規則如下:

  當新任務在方法 execute(java.lang.Runnable) 中提交時

  1) 如果運行的線程少於 corePoolSize,則創建新線程來處理請求,即使其他輔助線程是空閑的;

  2) 如果設置的corePoolSize 和 maximumPoolSize相同,則創建的線程池是大小固定的,

    如果運行的線程與corePoolSize相同,當有新請求過來時,若workQueue未滿,則將請求放入workQueue中,等待有空閑的線程去從workQueue中取任務並處理

  3) 如果運行的線程多於 corePoolSize 而少於 maximumPoolSize,則僅當隊列滿時才創建新線程才創建新的線程去處理請求;

  4) 如果運行的線程多於corePoolSize 並且等於maximumPoolSize,若隊列已經滿了,則通過handler所指定的策略來處理新請求;

  5) 如果將 maximumPoolSize 設置為基本的無界值(如 Integer.MAX_VALUE),則允許池適應任意數量的並發任務

   結論(摘自網絡):

也就是說,處理任務的優先級為:
1. 核心線程corePoolSize > 任務隊列workQueue > 最大線程maximumPoolSize,如果三者都滿了,使用handler處理被拒絕的任務。
2. 當池子的線程數大於corePoolSize的時候,多余的線程會等待keepAliveTime長的時間,如果無請求可處理就自行銷毀。

3>keepAliveTime :線程池維護線程所允許的空閑時間

 

4>unit : 線程池維護線程所允許的空間時間的單位

 

5>workQueue :線程池所使用的緩沖隊列,該緩沖隊列的長度決定了能夠緩沖的最大數量,緩沖隊列有三種通用策略:

  1) 直接提交。工作隊列的默認選項是 SynchronousQueue,它將任務直接提交給線程而不保持它們。在此,如果不存在可用於立即運行任務的線程,則試圖把任務加入隊列將失敗,因此會構造一個新的線程。此策略可以避免在處理可能具有內部依賴性的請求集時出現鎖。直接提交通常要求無界 maximumPoolSizes 以避免拒絕新提交的任務。當命令以超過隊列所能處理的平均數連續到達時,此策略允許無界線程具有增長的可能性;

 

  2) 無界隊列。使用無界隊列(例如,不具有預定義容量的 LinkedBlockingQueue)將導致在所有 corePoolSize 線程都忙時新任務在隊列中等待。這樣,創建的線程就不會超過 corePoolSize。(因此,maximumPoolSize 的值也就無效了。)當每個任務完全獨立於其他任務,即任務執行互不影響時,適合於使用無界隊列;例如,在 Web 頁服務器中。這種排隊可用於處理瞬態突發請求,當命令以超過隊列所能處理的平均數連續到達時,此策略允許無界線程具有增長的可能性;

 

  3) 有界隊列。當使用有限的 maximumPoolSizes 時,有界隊列(如 ArrayBlockingQueue)有助於防止資源耗盡,但是可能較難調整和控制。隊列大小和最大池大小可能需要相互折衷:使用大型隊列和小型池可以最大限度地降低 CPU 使用率、操作系統資源和上下文切換開銷,但是可能導致人工降低吞吐量。如果任務頻繁阻塞(例如,如果它們是 I/O 邊界),則系統可能為超過您許可的更多線程安排時間。使用小型隊列通常要求較大的池大小,CPU 使用率較高,但是可能遇到不可接受的調度開銷,這樣也會降低吞吐量.

 

6>被拒絕的任務:當Executor已經關閉(即執行了executorService.shutdown()方法后),並且Executor將有限邊界用於最大線程和工作隊列容量,且已經飽和時,在方法execute()中提交的新任務將被拒絕.

  在以上述情況下,execute 方法將調用其 RejectedExecutionHandler 的 RejectedExecutionHandler.rejectedExecution(java.lang.Runnable, java.util.concurrent.ThreadPoolExecutor) 方法。下面提供了四種預定義的處理程序策略:

    1) 在默認的 ThreadPoolExecutor.AbortPolicy 中,處理程序遭到拒絕將拋出運行時 RejectedExecutionException;
    2) 在 ThreadPoolExecutor.CallerRunsPolicy 中,線程調用運行該任務的 execute 本身。此策略提供簡單的反饋控制機制,能夠減緩新任務的提交速度

    3) 在 ThreadPoolExecutor.DiscardPolicy 中,不能執行的任務將被刪除;

    4) 在 ThreadPoolExecutor.DiscardOldestPolicy 中,如果執行程序尚未關閉,則位於工作隊列頭部的任務將被刪除,然后重試執行程序(如果再次失敗,則重復此過程)。

 

  7>創建新線程 : 使用 ThreadFactory 創建新線程。如果沒有另外說明,則在同一個 ThreadGroup 中一律使用 Executors.defaultThreadFactory() 創建線程,並且這些線程具有相同的 NORM_PRIORITY 優先級和非守護進程狀態。通過提供不同的 ThreadFactory,可以改變線程的名稱、線程組、優先級、守護進程狀態,等等。如果從 newThread 返回 null 時 ThreadFactory 未能創建線程,則執行程序將繼續運行,但不能執行任何任務。

  4.3 Executors

  Executors已經為程序員們預定義了大多數使用情景適用的線程池配置,強烈推薦使用這個類來創建對應的線程池。下面我們來介紹一下,該類中常用的幾個創建線程池方法。

  1>CachedThreadPool :該線程池比較適合沒有固定大小並且比較快速就能完成的小任務,它將為每個任務創建一個線程。那這樣子它與直接創建線程對象(new Thread())有什么區別呢?看到它的第三個參數60L和第四個參數TimeUnit.SECONDS了嗎?好處就在於60秒內能夠重用已創建的線程。下面是Executors中的newCachedThreadPool()的源代碼:

  

  2> FixedThreadPool使用的Thread對象的數量是有限的,如果提交的任務數量大於限制的最大線程數,那么這些任務講排隊,然后當有一個線程的任務結束之后,將會根據調度策略繼續等待執行下一個任務。下面是Executors中的newFixedThreadPool()的源代碼:

  

  3>SingleThreadExecutor就是線程數量為1的FixedThreadPool,如果提交了多個任務,那么這些任務將會排隊,每個任務都會在下一個任務開始之前運行結束,所有的任務將會使用相同的線程。下面是Executors中的newSingleThreadExecutor()的源代碼:

  

  好啦,了解了這三個配置的線程池,不知道大家有沒有自習看他們調用ThreadPoolExecutor的構造方法呢?

  通過三個配置的線程池的創建方法源代碼,我們可以發現:

  1> 除了CachedThreadPool使用的是直接提交策略的緩沖隊列以外,其余兩個用的采用的都是無界緩沖隊列,也就說,FixedThreadPool和SingleThreadExecutor創建的線程數量就不會超過 corePoolSize。

  2> 我們可以再來看看三個線程池采用的ThreadPoolExecutor構造方法都是同一個,使用的都是默認的ThreadFactory和handler:

 1 private static final RejectedExecutionHandler defaultHandler =
 2     new AbortPolicy();
 3 
 4 public ThreadPoolExecutor(int corePoolSize,
 5                     int maximumPoolSize,
 6                     long keepAliveTime,
 7                     TimeUnit unit,
 8                     BlockingQueue<Runnable> workQueue) {
 9     this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
10         Executors.defaultThreadFactory(), defaultHandler);
11 }

  也就說三個線程池創建的線程對象都是同組,優先權等級為正常的Thread.NORM_PRIORITY(5)的非守護線程,使用的被拒絕任務處理方式是直接拋出異常的AbortPolicy策略(前面有介紹)。

  大概了解到這里吧,下面我們給出一個運行例子,在方法中,分別給出了三種配置的線程池的測試方法,大家要測試哪種只要取消哪行的方法注釋然后注釋掉其他兩個運行即可,由於篇幅問題,具體的運行結果就不貼出來啦,eg:

TaskThread.java
View Code
 1 package com.thread.pool;
 2 
 3 public class TaskThread implements Runnable {
 4     protected int countDown = 10;    //DEFAULT
 5     private static int taskCount = 0;    //任務的個數
 6     private final int id = taskCount++;    //以第幾個作為當前任務的ID
 7     
 8     public TaskThread() { }
 9     
10     public TaskThread(int countDown) {
11         this.countDown = countDown;
12     }
13     
14     public String status() {
15         String name = Thread.currentThread().getName();
16         return name + "#" + id + "( " + (countDown > 0 ? countDown : "LifeOff") +" )" ;
17     }
18     
19     @Override
20     public void run() {
21         while (countDown-- > 0) {
22             System.out.println(status());
23             Thread.yield();
24         }
25     }
26 }

TestThreadPool.java

View Code
 1 package com.thread.pool;
 2  
 3  import java.util.concurrent.ExecutorService;
 4  import java.util.concurrent.Executors;
 5  
 6  public class TestThreadPool {
 7      
 8      public static void main(String[] args) {
 9  //        testCachedThreadPool();
10  //        testFixedThreadPool(0);
11          testSingleThread();
12      }
13      
14      /**
15       * 創建Runnable任務並添加到線程池中
16       * @param executorService    指定的線程池類型
17       */
18      public static void createTask(ExecutorService executorService) {
19          for (int i = 0; i < 5; i++) {
20              executorService.execute(new TaskThread());    //創建任務並交給線程池進行管理
21          }
22          executorService.shutdown();    //啟動一次順序關閉,執行以前提交的任務,但不接受新任務
23      }
24      
25      /**
26       * CachedThreadPool將為每個任務創建一個線程
27       */
28      public static void testCachedThreadPool() {
29          ExecutorService executorService = Executors.newCachedThreadPool();    //創建CachedThreadPool
30          createTask(executorService);
31      }
32      
33      /**
34       * FixedThreadPool使用的Thrad對象的數量是有限的,如果提交
35       * 的任務數量大於限制的最大線程數,那么這些任務講排隊,然
36       * 后當有一個線程的任務結束之后,將會根據調度策略繼續等待
37       * 執行下一個任務
38       * @param number 限制 FixedThreadPool 中的線程對象的數量
39       */
40      public static void testFixedThreadPool(int number) {
41          if (number == 0) {
42              number = 3;    //DEFAULT
43          }
44          ExecutorService executorService = Executors.newFixedThreadPool(number);
45          createTask(executorService);
46      }
47      
48      /**
49       * SingleThreadExecutor就是線程數量為1的FixedThreadPool。
50       * 如果提交了多個任務,那么這些任務將會排隊,每個任務都會在
51       * 下一個任務開始之前運行結束,所有的任務將會使用相同的線程
52       */
53      public static void testSingleThread() {
54          ExecutorService executorService = Executors.newSingleThreadExecutor();
55          createTask(executorService);
56      }
57  }

 

 參考資料:

1.《Java編程思想》第4版 P656

2.詩劍書生的專欄 :http://blog.csdn.net/axman/article/details/1481197

3.狂飆的蝸牛:http://blog.csdn.net/xjtuse_mal/article/details/5687368

4.洞玄的博客:http://dongxuan.iteye.com/blog/901689

5.Java官網文檔:http://docs.oracle.com/javase/6/docs/api/

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM