1. 程式人生 > >沈澱再出發:再談java的多線程機制

沈澱再出發:再談java的多線程機制

出發 syn ole 主線程 super close 返回 目標 aps

沈澱再出發:再談java的多線程機制

一、前言

自從我們學習了操作系統之後,對於其中的線程和進程就有了非常深刻的理解,但是,我們可能在C,C++語言之中嘗試過這些機制,並且做過相應的實驗,但是對於java的多線程機制以及其中延伸出來的很多概念和相應的實現方式一直都是模棱兩可的,雖然後來在面試的時候可能惡補了一些這方面的知識,但是也只是當時記住了,或者了解了一些,等到以後就會變得越來越淡忘了,比如線程的實現方式有兩三種,線程池的概念,線程的基本生命周期等等,以及關於線程之間的多並發引起的資源的搶占和競爭,鎖的出現,同步和異步,阻塞等等,這些概念再往下面延伸就到了jvm這種虛擬機的內存管理層面上了,由此又出現了jvm的生存周期,內存組成,函數調用,堆和棧,緩存,volatile共享變量等等機制,至此我們才能很好的理解多線程和並發。

二、java的多線程初探

2.1、進程和線程的生命周期

讓我們看看網上對多線程生命周期的描述:

技術分享圖片

Java線程具有五中基本狀態:

1  新建狀態(New):當線程對象對創建後,即進入了新建狀態,如:Thread t = new MyThread();
2  就緒狀態(Runnable):當調用線程對象的start()方法(t.start();),線程即進入就緒狀態。處於就緒狀態的線程,只是說明此線程已經做好了準備,
隨時等待CPU調度執行,並不是說執行了t.start()此線程立即就會執行;
3 運行狀態(Running):當CPU開始調度處於就緒狀態的線程時,此時線程才得以真正執行,即進入到運行狀態。
註:就緒狀態是進入到運行狀態的唯一入口,也就是說,線程要想進入運行狀態執行,首先必須處於就緒狀態中;
4 阻塞狀態(Blocked):處於運行狀態中的線程由於某種原因,暫時放棄對CPU的使用權,停止執行,此時進入阻塞狀態,直到其進入到就緒狀態,才有機會再次被CPU調用以進入到運行狀態。
根據阻塞產生的原因不同,阻塞狀態又可以分為三種:
5 1.等待阻塞:運行狀態中的線程執行wait()方法,使本線程進入到等待阻塞狀態; 6 2.同步阻塞 -- 線程在獲取synchronized同步鎖失敗(因為鎖被其它線程所占用),它會進入同步阻塞狀態;
7 3.其他阻塞 -- 通過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態。
當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。 8 死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命周期。

這種解釋其實和我們在操作系統中學習的是一致的,只不過內部的實現方式有所不同而已,同樣的如果實在Linux之中,進程和線程的生命周期有略微有所不同,但是究其根源來說都是這幾種步驟,只不過在某種過程之下可能有所細分而已。

再比如說其他資料上對java的多線程生命周期的劃分,我們也可以看到就是把其中的阻塞狀態分離出來而已:

技術分享圖片

明白了這一點,對於我們繼續細分其中的狀態背後的意義至關重要。

2.2、多線程狀態的實現

2.2.1、start()

新啟一個線程執行其run()方法,一個線程只能start一次。主要是通過調用native start0()來實現。

 1 public synchronized void start() {
 2      //判斷是否首次啟動
 3         if (threadStatus != 0)
 4             throw new IllegalThreadStateException();
 5 
 6         group.add(this);
 7 
 8         boolean started = false;
 9         try {
10        //啟動線程
11             start0();
12             started = true;
13         } finally {
14             try {
15                 if (!started) {
16                     group.threadStartFailed(this);
17                 }
18             } catch (Throwable ignore) {
19                 /* do nothing. If start0 threw a Throwable then
20                   it will be passed up the call stack */
21             }
22         }
23     }
24     private native void start0();

2.2.2、run()

run()方法是不需要用戶來調用的,當通過start方法啟動一個線程之後,當該線程獲得了CPU執行時間,便進入run方法體去執行具體的任務。註意,如果繼承Thread類則必須重寫run方法,在run方法中定義具體要執行的任務。

2.2.3 sleep()

sleep方法有兩個重載版本:

1  sleep(long millis)     //參數為毫秒
2  sleep(long millis,int nanoseconds)    //第一參數為毫秒,第二個參數為納秒

sleep相當於讓線程睡眠,交出CPU,讓CPU去執行其他的任務。但是有一點要非常註意,sleep方法不會釋放鎖,也就是說如果當前線程持有對某個對象的鎖,則即使調用sleep方法,其他線程也無法訪問這個對象。

2.2.4 yield()

調用yield方法會讓當前線程交出CPU權限,讓CPU去執行其他的線程。它跟sleep方法類似,同樣不會釋放鎖。但是yield不能控制具體的交出CPU的時間,另外,yield方法只能讓擁有相同優先級的線程有獲取CPU執行時間的機會。註意,調用yield方法並不會讓線程進入阻塞狀態,而是讓線程重回就緒狀態,它只需要等待重新獲取CPU執行時間,這一點是和sleep方法不一樣的。

2.2.5 join()

join方法有三個重載版本:

1  join()
2  join(long millis)     //參數為毫秒
3  join(long millis,int nanoseconds)    //第一參數為毫秒,第二個參數為納秒

join()實際是利用了wait(),只不過它不用等待notify()/notifyAll(),且不受其影響。它結束的條件是:1)等待時間到;2)目標線程已經run完(通過isAlive()來判斷)。

 1 public final synchronized void join(long millis) throws InterruptedException {
 2     long base = System.currentTimeMillis();
 3     long now = 0;
 4 
 5     if (millis < 0) {
 6         throw new IllegalArgumentException("timeout value is negative");
 7     }
 8     
 9     //0則需要一直等到目標線程run完
10     if (millis == 0) {
11         while (isAlive()) {
12             wait(0);
13         }
14     } else {
15         //如果目標線程未run完且阻塞時間未到,那麽調用線程會一直等待。
16         while (isAlive()) {
17             long delay = millis - now;
18             if (delay <= 0) {
19                 break;
20             }
21             wait(delay);
22             now = System.currentTimeMillis() - base;
23         }
24     }
25 }

2.2.6、interrupt()

此操作會中斷等待中的線程,並將線程的中斷標誌位置位。如果線程在運行態則不會受此影響
可以通過以下三種方式來判斷中斷:

1)isInterrupted()
    此方法只會讀取線程的中斷標誌位,並不會重置。
2)interrupted()
   此方法讀取線程的中斷標誌位,並會重置。
3)throw InterruptException
   拋出該異常的同時,會重置中斷標誌位。

2.2.6.1、終止處於“阻塞狀態”的線程

通常,我們通過“中斷”方式終止處於“阻塞狀態”的線程。當線程由於被調用了sleep(), wait(), join()等方法而進入阻塞狀態;若此時調用線程的interrupt()將線程的中斷標記設為true。由於處於阻塞狀態,中斷標記會被清除,同時產生一個InterruptedException異常。將InterruptedException放在適當的為止就能終止線程,形式如下:

 1 @Override
 2 public void run() {
 3     try {
 4         while (true) {
 5             // 執行任務...
 6         }
 7     } catch (InterruptedException ie) {  
 8         // 由於產生InterruptedException異常,退出while(true)循環,線程終止!
 9     }
10 }

在while(true)中不斷的執行任務,當線程處於阻塞狀態時,調用線程的interrupt()產生InterruptedException中斷。中斷的捕獲在while(true)之外,這樣就退出了while(true)循環!對InterruptedException的捕獲務一般放在while(true)循環體的外面,這樣,在產生異常時就退出了while(true)循環。否則,InterruptedException在while(true)循環體之內,就需要額外的添加退出處理。

 1 @Override
 2 public void run() {
 3     while (true) {
 4         try {
 5             // 執行任務...
 6         } catch (InterruptedException ie) {  
 7             // InterruptedException在while(true)循環體內。
 8             // 當線程產生了InterruptedException異常時,while(true)仍能繼續運行!需要手動退出
 9             break;
10         }
11     }
12 }

上面的InterruptedException異常的捕獲在whle(true)之內。當產生InterruptedException異常時,被catch處理之外,仍然在while(true)循環體內;要退出while(true)循環體,需要額外的執行退出while(true)的操作。

2.2.6.2、 終止處於“運行狀態”的線程

通常,我們通過“標記”方式終止處於“運行狀態”的線程。其中,包括“中斷標記”和“額外添加標記”。

通過“中斷標記”終止線程:

1 @Override
2 public void run() {
3     while (!isInterrupted()) {
4         // 執行任務...
5     }
6 }

isInterrupted()是判斷線程的中斷標記是不是為true。當線程處於運行狀態,並且我們需要終止它時;可以調用線程的interrupt()方法,使用線程的中斷標記為true,即isInterrupted()會返回true。此時,就會退出while循環。註意interrupt()並不會終止處於“運行狀態”的線程!它會將線程的中斷標記設為true。
通過“額外添加標記”終止處於“運行狀態”的線程,線程中有一個flag標記,它的默認值是true;並且我們提供stopTask()來設置flag標記。當我們需要終止該線程時,調用該線程的stopTask()方法就可以讓線程退出while循環。註意將flag定義為volatile類型,是為了保證flag的可見性。即其它線程通過stopTask()修改了flag之後,本線程能看到修改後的flag的值。

 1 private volatile boolean flag= true;
 2 protected void stopTask() {
 3     flag = false;
 4 }
 5 @Override
 6 public void run() {
 7     while (flag) {
 8         // 執行任務...
 9     }
10 }

綜合線程處於“阻塞狀態”和“運行狀態”的終止方式,比較通用的終止線程的形式如下:

 1 @Override
 2 public void run() {
 3     try {
 4         // 1. isInterrupted()保證,只要中斷標記為true就終止線程。
 5         while (!isInterrupted()) {
 6             // 執行任務...
 7         }
 8     } catch (InterruptedException ie) {  
 9         // 2. InterruptedException異常保證,當InterruptedException異常產生時,線程被終止。
10     }
11 }

正常中斷並退出的案例:

技術分享圖片
 1 package com.thread.test;
 2 
 3 class MyThread extends Thread {
 4     
 5     public MyThread(String name) {
 6         super(name);
 7     }
 8 
 9     @Override
10     public void run() {
11         try {  
12             int i=0;
13             while (!isInterrupted()) {
14                 Thread.sleep(100); // 休眠100ms
15                 i++;
16                 System.out.println(Thread.currentThread().getName()+" ("+this.getState()+") loop " + i);  
17             }
18         } catch (InterruptedException e) {  
19             System.out.println(Thread.currentThread().getName() +" ("+this.getState()+") catch InterruptedException.");  
20         }
21     }
22 }
23 
24 public class Test1 {
25 
26     public static void main(String[] args) {  
27         try {  
28             Thread t1 = new MyThread("t1");  // 新建“線程t1”
29             System.out.println(t1.getName() +" ("+t1.getState()+") is new.");  
30 
31             t1.start();                      // 啟動“線程t1”
32             System.out.println(t1.getName() +" ("+t1.getState()+") is started.");  
33 
34             // 主線程休眠300ms,然後主線程給t1發“中斷”指令。
35             Thread.sleep(300);
36             t1.interrupt();
37             System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted.");
38 
39             // 主線程休眠300ms,然後查看t1的狀態。
40             Thread.sleep(300);
41             System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted now.");
42         } catch (InterruptedException e) {  
43             e.printStackTrace();
44         }
45     } 
46 }
中斷結束線程

技術分享圖片

中斷之後死循環的案例:

技術分享圖片
 1 package com.thread.test;
 2 
 3 class MyThread1 extends Thread {
 4  
 5  public MyThread1(String name) {
 6      super(name);
 7  }
 8 
 9  @Override
10  public void run() {
11      int i=0;
12      while (!isInterrupted()) {
13          try {
14              Thread.sleep(100); // 休眠100ms
15          } catch (InterruptedException ie) {  
16              System.out.println(Thread.currentThread().getName() +" ("+this.getState()+") catch InterruptedException.");  
17          }
18          i++;
19          System.out.println(Thread.currentThread().getName()+" ("+this.getState()+") loop " + i);  
20      }
21  }
22 }
23 
24 public class Test2 {
25 
26  public static void main(String[] args) {  
27      try {  
28          Thread t1 = new MyThread1("t1");  // 新建“線程t1”
29          System.out.println(t1.getName() +" ("+t1.getState()+") is new.");  
30 
31          t1.start();                      // 啟動“線程t1”
32          System.out.println(t1.getName() +" ("+t1.getState()+") is started.");  
33 
34          // 主線程休眠300ms,然後主線程給t1發“中斷”指令。
35          Thread.sleep(300);
36          t1.interrupt();
37          System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted.");
38 
39          // 主線程休眠300ms,然後查看t1的狀態。
40          Thread.sleep(300);
41          System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted now.");
42      } catch (InterruptedException e) {  
43          e.printStackTrace();
44      }
45  } 
46 }
死循環 技術分享圖片
 1 t1 (NEW) is new.
 2 t1 (RUNNABLE) is started.
 3 t1 (RUNNABLE) loop 1
 4 t1 (RUNNABLE) loop 2
 5 t1 (TIMED_WAITING) is interrupted.
 6 t1 (RUNNABLE) catch InterruptedException.
 7 t1 (RUNNABLE) loop 3
 8 t1 (RUNNABLE) loop 4
 9 t1 (RUNNABLE) loop 5
10 t1 (TIMED_WAITING) is interrupted now.
11 t1 (RUNNABLE) loop 6
12 t1 (RUNNABLE) loop 7
13 t1 (RUNNABLE) loop 8
14 t1 (RUNNABLE) loop 9
15 t1 (RUNNABLE) loop 10
16 t1 (RUNNABLE) loop 11
17 t1 (RUNNABLE) loop 12
18 t1 (RUNNABLE) loop 13
19 t1 (RUNNABLE) loop 14
20 t1 (RUNNABLE) loop 15
21 t1 (RUNNABLE) loop 16
22 t1 (RUNNABLE) loop 17
23 t1 (RUNNABLE) loop 18
24 t1 (RUNNABLE) loop 19
25 t1 (RUNNABLE) loop 20
26 t1 (RUNNABLE) loop 21
27 t1 (RUNNABLE) loop 22
28 t1 (RUNNABLE) loop 23
29 t1 (RUNNABLE) loop 24
30 t1 (RUNNABLE) loop 25
31 t1 (RUNNABLE) loop 26
32 t1 (RUNNABLE) loop 27
33 t1 (RUNNABLE) loop 28
34 t1 (RUNNABLE) loop 29
35 t1 (RUNNABLE) loop 30
36 t1 (RUNNABLE) loop 31
37 t1 (RUNNABLE) loop 32
38 t1 (RUNNABLE) loop 33
39 t1 (RUNNABLE) loop 34
40 t1 (RUNNABLE) loop 35
41 t1 (RUNNABLE) loop 36
42 。。。。。。
View Code

技術分享圖片

程序進入了死循環,這是因為t1在“等待(阻塞)狀態”時,被interrupt()中斷;此時,會清除中斷標記[即isInterrupted()會返回false],而且會拋出InterruptedException異常(該異常在while循環體內被捕獲)。因此,t1理所當然的會進入死循環了。解決該問題,需要我們在捕獲異常時,額外的進行退出while循環的處理。例如,在MyThread的catch(InterruptedException)中添加break 或 return就能解決該問題。

解決方案:

技術分享圖片
 1 package com.thread.test;
 2 
 3 class MyThread3 extends Thread {
 4 
 5  private volatile boolean flag= true;
 6  public void stopTask() {
 7      flag = false;
 8  }
 9  
10  public MyThread3(String name) {
11      super(name);
12  }
13 
14  @Override
15  public void run() {
16      synchronized(this) {
17          try {
18              int i=0;
19              while (flag) {
20                  Thread.sleep(100); // 休眠100ms
21                  i++;
22                  System.out.println(Thread.currentThread().getName()+" ("+this.getState()+") loop " + i);  
23              }
24          } catch (InterruptedException ie) {  
25              System.out.println(Thread.currentThread().getName() +" ("+this.getState()+") catch InterruptedException.");  
26          }
27      }  
28  }
29 }
30 
31 public class Test3 {
32 
33  public static void main(String[] args) {  
34      try {  
35          MyThread3 t1 = new MyThread3("t1");  // 新建“線程t1”
36          System.out.println(t1.getName() +" ("+t1.getState()+") is new.");  
37 
38          t1.start();                      // 啟動“線程t1”
39          System.out.println(t1.getName() +" ("+t1.getState()+") is started.");  
40 
41          // 主線程休眠300ms,然後主線程給t1發“中斷”指令。
42          Thread.sleep(300);
43          t1.stopTask();
44          System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted.");
45 
46          // 主線程休眠300ms,然後查看t1的狀態。
47          Thread.sleep(300);
48          System.out.println(t1.getName() +" ("+t1.getState()+") is interrupted now.");
49      } catch (InterruptedException e) {  
50          e.printStackTrace();
51      }
52  } 
53 }
使用特殊標誌

技術分享圖片

2.2.7、suspend()/resume()

掛起線程,直到被resume,才會蘇醒。但調用suspend()的線程和調用resume()的線程,可能會因為爭鎖的問題而發生死鎖,所以JDK 7開始已經不推薦使用了。Thread中的stop()和suspend()方法,由於固有的不安全性,已經建議不再使用!

參考文獻:https://www.cnblogs.com/skywang12345/p/3479949.html

https://www.cnblogs.com/lwbqqyumidi/p/3804883.html

沈澱再出發:再談java的多線程機制