1. 程式人生 > >多執行緒與併發程式設計

多執行緒與併發程式設計

前言

多執行緒併發程式設計是Java程式設計中重要的一塊內容,也是面試重點覆蓋區域,所以學好多執行緒併發程式設計對我們來說極其重要,下面跟我一起開啟本次的學習之旅吧。

正文

執行緒與程序

1 執行緒:程序中負責程式執行的執行單元
執行緒本身依靠程式進行執行
執行緒是程式中的順序控制流,只能使用分配給程式的資源和環境

2 程序:執行中的程式
一個程序至少包含一個執行緒

3 單執行緒:程式中只存在一個執行緒,實際上主方法就是一個主執行緒

4 多執行緒:在一個程式中執行多個任務
目的是更好地使用CPU資源

執行緒的實現

繼承Thread類

java.lang包中定義, 繼承Thread類必須重寫run()方法

1 2 3 4 5 6 7 8 9 10 11 12
class MyThread extends Thread{      private static int num = 0 ;        public MyThread(){         
num++;      }        @Override      public void run() {          System.out.println( "主動建立的第" +num+ "個執行緒" );      } }

建立好了自己的執行緒類之後,就可以建立執行緒物件了,然後通過start()方法去啟動執行緒。注意,不是呼叫run()方法啟動執行緒,run方法中只是定義需要執行的任務,如果呼叫run方法,即相當於在主執行緒中執行run方法,跟普通的方法呼叫沒有任何區別,此時並不會建立一個新的執行緒來執行定義的任務。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
public class Test {      public static void main(String[] args)  {          MyThread thread = new MyThread();          thread.start();      } } class MyThread extends Thread{      private static int num = 0 ;      public MyThread(){          num++;      }      @Override      public void run() {          System.out.println( "主動建立的第" +num+ "個執行緒" );      } }

在上面程式碼中,通過呼叫start()方法,就會建立一個新的執行緒了。為了分清start()方法呼叫和run()方法呼叫的區別,請看下面一個例子:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
public class Test {      public static void main(String[] args)  {          System.out.println( "主執行緒ID:" +Thread.currentThread().getId());          MyThread thread1 = new MyThread( "thread1" );          thread1.start();          MyThread thread2 = new MyThread( "thread2" );          thread2.run();      } }   class MyThread extends Thread{      private String name;        public MyThread(String name){          this .name = name;      }        @Override      public void run() {          System.out.println( "name:" +name+ " 子執行緒ID:" +Thread.currentThread().getId());      } }

執行結果:

 

 

從輸出結果可以得出以下結論:

1)thread1和thread2的執行緒ID不同,thread2和主執行緒ID相同,說明通過run方法呼叫並不會建立新的執行緒,而是在主執行緒中直接執行run方法,跟普通的方法呼叫沒有任何區別;

2)雖然thread1的start方法呼叫在thread2的run方法前面呼叫,但是先輸出的是thread2的run方法呼叫的相關資訊,說明新執行緒建立的過程不會阻塞主執行緒的後續執行。

實現Runnable介面

在Java中建立執行緒除了繼承Thread類之外,還可以通過實現Runnable介面來實現類似的功能。實現Runnable介面必須重寫其run方法。
下面是一個例子:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
public class Test {      public static void main(String[] args)  {          System.out.println( "主執行緒ID:" +Thread.currentThread().getId());          MyRunnable runnable = new MyRunnable();          Thread thread = new Thread(runnable);          thread.start();      } } class MyRunnable implements Runnable{      public MyRunnable() {      }        @Override      public void run() {          System.out.println( "子執行緒ID:" +Thread.currentThread().getId());      } }

Runnable的中文意思是“任務”,顧名思義,通過實現Runnable介面,我們定義了一個子任務,然後將子任務交由Thread去執行。注意,這種方式必須將Runnable作為Thread類的引數,然後通過Thread的start方法來建立一個新執行緒來執行該子任務。如果呼叫Runnable的run方法的話,是不會建立新執行緒的,這根普通的方法呼叫沒有任何區別。

事實上,檢視Thread類的實現原始碼會發現Thread類是實現了Runnable介面的。

在Java中,這2種方式都可以用來建立執行緒去執行子任務,具體選擇哪一種方式要看自己的需求。直接繼承Thread類的話,可能比實現Runnable介面看起來更加簡潔,但是由於Java只允許單繼承,所以如果自定義類需要繼承其他類,則只能選擇實現Runnable介面。

使用ExecutorService、Callable、Future實現有返回結果的多執行緒

多執行緒後續會學到,這裡暫時先知道一下有這種方法即可。

ExecutorService、Callable、Future這個物件實際上都是屬於Executor框架中的功能類。想要詳細瞭解Executor框架的可以訪問http://www.javaeye.com/topic/366591 ,這裡面對該框架做了很詳細的解釋。返回結果的執行緒是在JDK1.5中引入的新特徵,確實很實用,有了這種特徵我就不需要再為了得到返回值而大費周折了,而且即便實現了也可能漏洞百出。

可返回值的任務必須實現Callable介面,類似的,無返回值的任務必須Runnable介面。執行Callable任務後,可以獲取一個Future的物件,在該物件上呼叫get就可以獲取到Callable任務返回的Object了,再結合線程池介面ExecutorService就可以實現傳說中有返回結果的多執行緒了。下面提供了一個完整的有返回結果的多執行緒測試例子,在JDK1.5下驗證過沒問題可以直接使用。程式碼如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
/** * 有返回值的執行緒 */  @SuppressWarnings ( "unchecked" public class Test {  public static void main(String[] args) throws ExecutionException,       InterruptedException {      System.out.println( "----程式開始執行----" );      Date date1 = new Date();        int taskSize = 5     // 建立一個執行緒池      ExecutorService pool = Executors.newFixedThreadPool(taskSize);      // 建立多個有返回值的任務      List<Future> list = new ArrayList<Future>();      for ( int i = 0 ; i < taskSize; i++) {       Callable c = new MyCallable(i + " " );       // 執行任務並獲取Future物件       Future f = pool.submit(c);       // System.out.println(">>>" + f.get().toString());       list.add(f);          // 關閉執行緒池      pool.shutdown();        // 獲取所有併發任務的執行結果      for (Future f : list) {       // 從Future物件上獲取任務的返回值,並輸出到控制檯       System.out.println( ">>>" + f.get().toString());            Date date2 = new Date();      System.out.println( "----程式結束執行----,程式執行時間【"        + (date2.getTime() - date1.getTime()) + "毫秒】" );    class MyCallable implements Callable<Object> {  private String taskNum;    MyCallable(String taskNum) {      this .taskNum = taskNum;    public Object call() throws Exception {      System.out.println( ">>>" + taskNum + "任務啟動" );      Date dateTmp1 = new Date();      Thread.sleep( 1000 );      Date dateTmp2 = new Date();      long time = dateTmp2.getTime() - dateTmp1.getTime();      System.out.println( ">>>" + taskNum + "任務終止" );      return taskNum + "任務返回執行結果,當前任務時間【" + time + "毫秒】" } }

程式碼說明:
上述程式碼中Executors類,提供了一系列工廠方法用於創先執行緒池,返回的執行緒池都實現了ExecutorService介面。
public static ExecutorService newFixedThreadPool(int nThreads)
建立固定數目執行緒的執行緒池。

public static ExecutorService newCachedThreadPool()
建立一個可快取的執行緒池,呼叫execute 將重用以前構造的執行緒(如果執行緒可用)。如果現有執行緒沒有可用的,則建立一個新執行緒並新增到池中。終止並從快取中移除那些已有 60 秒鐘未被使用的執行緒。

public static ExecutorService newSingleThreadExecutor()
建立一個單執行緒化的Executor。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
建立一個支援定時及週期性的任務執行的執行緒池,多數情況下可用來替代Timer類。

ExecutoreService提供了submit()方法,傳遞一個Callable,或Runnable,返回Future。如果Executor後臺執行緒池還沒有完成Callable的計算,這呼叫返回Future物件的get()方法,會阻塞直到計算完成。

執行緒的狀態

在正式學習Thread類中的具體方法之前,我們先來了解一下執行緒有哪些狀態,這個將會有助於後面對Thread類中的方法的理解。

  • 建立(new)狀態: 準備好了一個多執行緒的物件
  • 就緒(runnable)狀態: 呼叫了start()方法, 等待CPU進行排程
  • 執行(running)狀態: 執行run()方法
  • 阻塞(blocked)狀態: 暫時停止執行, 可能將資源交給其它執行緒使用
  • 終止(dead)狀態: 執行緒銷燬

當需要新起一個執行緒來執行某個子任務時,就建立了一個執行緒。但是執行緒建立之後,不會立即進入就緒狀態,因為執行緒的執行需要一些條件(比如記憶體資源,在前面的JVM記憶體區域劃分一篇博文中知道程式計數器、Java棧、本地方法棧都是執行緒私有的,所以需要為執行緒分配一定的記憶體空間),只有執行緒執行需要的所有條件滿足了,才進入就緒狀態。

當執行緒進入就緒狀態後,不代表立刻就能獲取CPU執行時間,也許此時CPU正在執行其他的事情,因此它要等待。當得到CPU執行時間之後,執行緒便真正進入執行狀態。

執行緒在執行狀態過程中,可能有多個原因導致當前執行緒不繼續執行下去,比如使用者主動讓執行緒睡眠(睡眠一定的時間之後再重新執行)、使用者主動讓執行緒等待,或者被同步塊給阻塞,此時就對應著多個狀態:time waiting(睡眠或等待一定的事件)、waiting(等待被喚醒)、blocked(阻塞)。

當由於突然中斷或者子任務執行完畢,執行緒就會被消亡。

下面這副圖描述了執行緒從建立到消亡之間的狀態:

在有些教程上將blocked、waiting、time waiting統稱為阻塞狀態,這個也是可以的,只不過這裡我想將執行緒的狀態和Java中的方法呼叫聯絡起來,所以將waiting和time waiting兩個狀態分離出來。

注:sleep和wait的區別:

  • sleepThread類的方法,waitObject類中定義的方法.
  • Thread.sleep不會導致鎖行為的改變, 如果當前執行緒是擁有鎖的, 那麼Thread.sleep不會讓執行緒釋放鎖.
  • Thread.sleepObject.wait都會暫停當前的執行緒. OS會將執行時間分配給其它執行緒. 區別是, 呼叫wait後, 需要別的執行緒執行notify/notifyAll才能夠重新獲得CPU執行時間.

上下文切換

對於單核CPU來說(對於多核CPU,此處就理解為一個核),CPU在一個時刻只能執行一個執行緒,當在執行一個執行緒的過程中轉去執行另外一個執行緒,這個叫做執行緒上下文切換(對於程序也是類似)。

由於可能當前執行緒的任務並沒有執行完畢,所以在切換時需要儲存執行緒的執行狀態,以便下次重新切換回來時能夠繼續切換之前的狀態執行。舉個簡單的例子:比如一個執行緒A正在讀取一個檔案的內容,正讀到檔案的一半,此時需要暫停執行緒A,轉去執行執行緒B,當再次切換回來執行執行緒A的時候,我們不希望執行緒A又從檔案的開頭來讀取。

因此需要記錄執行緒A的執行狀態,那麼會記錄哪些資料呢?因為下次恢復時需要知道在這之前當前執行緒已經執行到哪條指令了,所以需要記錄程式計數器的值,另外比如說執行緒正在進行某個計算的時候被掛起了,那麼下次繼續執行的時候需要知道之前掛起時變數的值時多少,因此需要記錄CPU暫存器的狀態。所以一般來說,執行緒上下文切換過程中會記錄程式計數器、CPU暫存器狀態等資料。

說簡單點的:對於執行緒的上下文切換實際上就是 儲存和恢復CPU狀態的過程,它使得執行緒執行能夠從中斷點恢復執行

雖然多執行緒可以使得任務執行的效率得到提升,但是由於線上程切換時同樣會帶來一定的開銷代價,並且多個執行緒會導致系統資源佔用的增加,所以在進行多執行緒程式設計時要注意這些因素。

執行緒的常用方法

編號 方法 說明
1 public void start() 使該執行緒開始執行;Java 虛擬機器呼叫該執行緒的 run 方法。 2 public void run() 如果該執行緒是使用獨立的 Runnable 執行物件構造的,則呼叫該 Runnable 物件的 run 方法;否則,該方法不執行任何操作並返回。 3 public final void setName(String name) 改變執行緒名稱,使之與引數 name 相同。 4 public final void setPriority(int priority) 更改執行緒的優先順序。 5 public final void setDaemon(boolean on) 將該執行緒標記為守護執行緒或使用者執行緒。 6 public final void join(long millisec) 等待該執行緒終止的時間最長為 millis 毫秒。 7 public void interrupt() 中斷執行緒。 8 public final boolean isAlive() 測試執行緒是否處於活動狀態。 9 public static void yield() 暫停當前正在執行的執行緒物件,並執行其他執行緒。 10 public static void sleep(long millisec) 在指定的毫秒數內讓當前正在執行的執行緒休眠(暫停執行),此操作受到系統計時器和排程程式精度和準確性的影響。 11 public static Thread currentThread() 返回對當前正在執行的執行緒物件的引用。

 

 

靜態方法

currentThread()方法

currentThread()方法可以返回程式碼段正在被哪個執行緒呼叫的資訊。

1 2 3 4 5
public class Run1{      public static void main(String[] args){                      System.out.println(Thread.currentThread().getName());      } }

sleep()方法

方法sleep()的作用是在指定的毫秒數內讓當前“正在執行的執行緒”休眠(暫停執行)。這個“正在執行的執行緒”是指this.currentThread()返回的執行緒。

sleep方法有兩個過載版本:

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

sleep相當於讓執行緒睡眠,交出CPU,讓CPU去執行其他的任務。
但是有一點要非常注意,sleep方法不會釋放鎖,也就是說如果當前執行緒持有對某個物件的鎖,則即使呼叫sleep方法,其他執行緒也無法訪問這個物件。看下面這個例子就清楚了:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
public class Test {        private int i = 10 ;      private Object object = new Object();        public static void main(String[] args) throws IOException  {          Test test = new Test();          MyThread thread1 = test. new MyThread();          MyThread thread2 = test. new MyThread();          thread1.start();          thread2.start();      }        class MyThread extends Thread{          @Override          public void run() {              synchronized (object) {                  i++;                  System.out.println( "i:" +i);                  try {                      System.out.println( "執行緒" +Thread.currentThread().getName()+ "進入睡眠狀態" );                      Thread.currentThread().sleep( 10000 );                  } catch (InterruptedException e) {                      // TODO: handle exception                  }                  System.out.println( "執行緒" +Thread.currentThread().getName()+ "睡眠結束" );                  i++;                  System.out.println( "i:" +i);              }          }      } }

輸出結果:

從上面輸出結果可以看出,當Thread-0進入睡眠狀態之後,Thread-1並沒有去執行具體的任務。只有當Thread-0執行完之後,此時Thread-0釋放了物件鎖,Thread-1才開始執行。

注意,如果呼叫了sleep方法,必須捕獲InterruptedException異常或者將該異常向上層丟擲。當執行緒睡眠時間滿後,不一定會立即得到執行,因為此時可能CPU正在執行其他的任務。所以說呼叫sleep方法相當於讓執行緒進入阻塞狀態。

yield()方法

呼叫yield方法會讓當前執行緒交出CPU許可權,讓CPU去執行其他的執行緒。它跟sleep方法類似,同樣不會釋放鎖。但是yield不能控制具體的交出CPU的時間,另外,yield方法只能讓擁有相同優先順序的執行緒有獲取CPU執行時間的機會。

注意,呼叫yield方法並不會讓執行緒進入阻塞狀態,而是讓執行緒重回就緒狀態,它只需要等待重新獲取CPU執行時間,這一點是和sleep方法不一樣的。
程式碼:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
public class MyThread  extends Thread{      @Override      public void run() {          long beginTime=System.currentTimeMillis();       &nb