1. 程式人生 > >java執行緒學習總結(一)

java執行緒學習總結(一)

(宣告:並非原創,只是一個簡單總結)

一、執行緒和程序的概念:

           程序:程序是處於執行過程中的程式,並且具有一定的對功能,是系統進行資源分配和排程的一個獨立單位。

           執行緒:執行緒是程序的組成部分,一個程序至少擁有一個執行緒;執行緒可以擁有自己的堆疊、自己的程式計數器和自己的區域性變數,但不擁有系統資源,它和父程序的其執行緒共享該程序的全部資源。(較為官方的定義)

以下解釋來源於知乎網友回答,覺得非常好,在此引用:

1、單程序單執行緒:一個人在一個桌子上吃菜。

2、單程序多執行緒:多個人在同一個桌子上吃菜。

3、多程序單執行緒:多個人每個人在自己的桌子上吃菜。

           a.多執行緒的問題就是多個人同時吃一道菜的時候發生爭搶,例如兩個人同時夾一道菜,一個人剛深處筷子,結果伸到的時候菜已經被夾走了。。此時就必須等一個人夾一口之後,再夾菜,也就是說資源共享就會引發衝突爭搶。

          b. 對於windows使用者來說,【開桌子】的開銷很大,因此windows鼓勵大家在一個桌子上吃菜。因此windows多執行緒學習的重點是面對資源爭搶與同步方面的問題。

          c.對於linux系統來說,【開桌子】的開銷很小,因此linux鼓勵大家儘量每個人都開自己的桌子吃菜。這帶來的新的問題是:坐在兩張不同的桌子上說話不方便。因此,linux下學習的重點是“程序”間的通訊方法。(這裡需要強調的是,linux下並沒有真正意義上的執行緒)

二、執行緒建立的三種方式以及對比(瘋狂java講義,P710)

          1.繼承自Thread類建立執行緒類

              (1).定義thread類的子類,並重寫該run方法,該run方法的執行體就代表了執行緒要完成的任務。因此run方法成為執行體。

              (2).建立了Thread子類例項,即建立了執行緒物件。

              (3).呼叫執行緒物件的start()方法來啟動該執行緒。

public class ThreadTest1 extends Thread{
	private int i;
	
	@Override
	public void run() {
		for (int i = 0; i < 100; i++) {
			System.out.println(this.getName()+"----"+i);
		}
	}
	
	public static void main(String[] args){
		for (int i = 0; i < 100; i++) {
			//main()執行,會建立主執行緒,主執行緒時候main()確定的
			System.out.println(Thread.currentThread().getName()+"===="+i);//獲取主執行緒的名字
			if(i==20){
				new ThreadTest1().start();//啟動第一個執行緒
				new ThreadTest1().start();//啟動第二個執行緒
			}
		}
	}
}
執行結果(部分):

main====20
main====21
main====22
Thread-0----0
Thread-1----0
main====23
Thread-1----1
Thread-0----1
Thread-0----2
         分析:主執行緒中存在判斷,當i==20的時候,就啟動第一個子執行緒Thread-0,但是為什麼會在i==22時,才啟動子執行緒呢?

這就要考慮到執行緒執行緒的生命週期了,執行緒有5種狀態:新建、就緒、執行、消亡、阻塞。new ThreadTest1(),執行緒就被創建出來了,當呼叫start()方法時,執行緒就會處於就緒狀態。處於就緒狀態的執行緒,只是說明此執行緒已經做好了準備,隨時等待CPU排程執行,並不是執行了t.start()此執行緒就會立即執行。(一步步深入,一口吃不成胖子)

        為什麼Thread-0和Thread-1兩個執行緒輸出的i不連續?使用繼承自Thread的方式來建立執行緒例項,多個執行緒之間是無法共享例項變數的。

        2.實現Runnable介面來建立執行緒類

           (1).定義Runnable介面的實現類,並重寫該介面的run方法,該run()方法同樣式該執行緒的執行體。

           (2).建立Runnable實現類的例項,並以此例項作為Thread的target來建立Thread物件,該Thread物件才是真正的執行緒物件。(感覺不好理解,結合程式碼就so easy了)

public class ThreadTest2 implements Runnable{
	
	private int i;
	
	@Override
	public void run() {
		for (; i < 100; i++) {
			System.out.println(Thread.currentThread().getName()+"----"+i);
		}
	}

	public static void main(String[] args) {
		for (int i = 0; i < 100; i++) {
			System.out.println(Thread.currentThread().getName()+"=="+i);
			if(i==20){
				ThreadTest2 target=new ThreadTest2();
				new Thread(target).start();//這裡:將ThreadTest2的物件作為,Thread類的target
				new Thread(target).start();//即new Thread(target)
			}
		}
	}
}
結果:
Thread-0----0
Thread-1----1
main==24
Thread-1----3
Thread-0----2
Thread-1----4
main==25
Thread-1----6
Thread-0----5
           分析:結果有兩個點很重要,第一點:i 輸出變成連續的了;第二點:i 輸出雖然連續,但是順序是亂的,為什麼?

           首先由這麼個規定:採用Runnable介面的方式建立的多個執行緒可以共享執行緒類的例項屬性。這是因為在這種方式下,程式所建立的Runnable物件只是執行緒的target,而多個執行緒可以共享一個target,所以多個執行緒可以共享同一個執行緒(實際是執行緒的target類)的例項屬性。

          為什麼i的輸出順序是亂的,執行緒獲取資源的方式:搶佔式策略。當Thread-0本該輸出Thread-0---2時,恰巧被Thread-1搶到了cpu,所以Thread-1---3先列印輸出。

          3.使用Callable和Future建立執行緒(這種建立執行緒的方式經常被忽略,我也搞不懂。。。)

           前面兩種方式已經很全面,很實用了,但是為什麼還要有第三種方式呢?書中這樣說到:通過實現Runnable介面建立多執行緒時,Thread類的作用就是把run()方法包裝成執行緒的執行體。那麼是否可以直接把任意方法包裝成執行緒的執行呢?Java目前不行!但是java的模仿者C#可以。受此啟發,從java5開始,java提供了Callable介面,該介面怎麼看都像是Runnable介面的增強版,Callable介面提供了一個call()方法可以作為執行緒的執行體,但call()方法比run()方法更強大。call()方法可以有返回值,可以宣告丟擲異常。
          因為Callable介面是java5新增的介面,而且不是Runnable的子介面,所以不能作為Thread的target,為此Java5提供了Future介面來代表Callable接口裡的call()方法的返回值,併為Future介面提供了一個FutureTask實現類,該實現類實現了Future介面,並實現了Runnable介面---可以作為Thread類的target.

          建立並啟動有返回值的執行緒步驟如下:

          (1)建立Callable介面的實現類,並實現call()方法,該call()方法將作為執行緒的執行體,且該方法有返回值。

          (2)建立Callable實現類的例項,使用FutureTask類來包裝Callable物件,該FutureTask物件封裝了該Callable物件的call()方法的返回值。

          (3)使用FutureTask物件作為Thread物件的target建立並啟動執行緒。

          (4)呼叫FutureTask物件的get()方法來獲得子執行緒結束後的返回值。

public class ThreadTest3 implements Callable<Integer>{
	
	//該方法作為執行緒的執行體
	@Override
	public Integer call() throws Exception {
		int i=0;
		for (; i < 100; i++) {
			System.out.println(Thread.currentThread().getName()+"---"+i);
		}
		return i;
	}
	
	public static void main(String[] args) {
		//建立Callable物件
		ThreadTest3 rt = new ThreadTest3();
		//使用FutureTask來包裝Callable物件
		FutureTask<Integer> task = new FutureTask<>(rt);
		for (int i = 0; i < 100; i++) {
			System.out.println(Thread.currentThread().getName()+"=="+i);
			if(i==20){
				new Thread(task).start();
			}
		}
		try{
			//獲取執行緒的返回值
			System.out.println("子執行緒的返回值"+task.get());
		}catch(Exception e){
			e.printStackTrace();
		}
		
	}	
}

           4. 建立執行緒三種方式的對比:

            採用實現Runnable、Callable介面的方式創見多執行緒時,優勢是:

            執行緒類只是實現了Runnable介面或Callable介面,還可以繼承其他類。

            在這種方式下,多個執行緒可以共享同一個target物件,所以非常適合多個相同執行緒來處理同一份資源的情況,從而可以將CPU、程式碼和資料分開,形成清晰的模型,較好地體現了面向物件的思想。

           劣勢是:

           程式設計稍微複雜,如果要訪問當前執行緒,則必須使用Thread.currentThread()方法。


           使用繼承Thread類的方式建立多執行緒時,優勢是

           編寫簡單,如果需要訪問當前執行緒,則無需使用Thread.currentThread()方法,直接使用this即可獲得當前執行緒。

           劣勢是

           執行緒類已經繼承了Thread類,所以不能再繼承其他父類。



三、執行緒的生命週期

         執行緒在被建立以後,在它的整個生命週期中,存在著以下五種狀態:新建(New)、就緒(Runnable)、執行(Running)、阻塞(Boocked)和死亡(Dead)。 

        1.新建和就緒狀態

            (1)當程式使用New關鍵字建立了一個執行緒之後,該執行緒就處於新建狀態,此時的執行緒物件沒有任何的動態特徵。

            (2)當執行緒物件呼叫了strat()方法之後,該執行緒處於就緒狀態,處於就緒狀態的執行緒並沒有真正的執行,至於什麼時候開始執行,取決於JVM裡執行緒排程器的排程。                 (3)注意事項:啟動執行緒是呼叫執行緒的start()方法,而不是run()方法,run()方法是執行緒的執行體---------執行緒要做的事情。如果我們呼叫了run()方法,會出現什麼結果呢?那麼就是執行緒物件被當作了普通物件,而執行緒的執行體run()方法,也被當成了普通的方法。程式碼如下:

public class ThreadRunTest implements Runnable{
	
	private int i=0;
	
	@Override
	public void run() {
		for (; i < 100; i++) {
			System.out.println(Thread.currentThread().getName()+"---"+i);
		}
	}
	
	public static void main(String[] args) {
		ThreadRunTest target = new ThreadRunTest();
			
		for (int i = 0; i < 100; i++) {
			System.out.println(Thread.currentThread().getName()+"==="+i);
			if(i==20){
				Thread thread = new Thread(target);
				new Thread(target).run();    //呼叫的不是start()方法了
				new Thread(target).run();
			}
		}
	}
}

            列印結果:

main===17
main===18
main===19
main===20  //這裡只是開始執行普通物件的普通方法
main---0
main---1
main---2
main---3
main---4
..........
main---96
main---97
main---98
main---99   //普通物件的普通方法即將執行完畢
main===21
main===22
main===23
main===24
main===25

              結果分析:從結果可以清晰的看出,整個程式只有一個主執行緒main,沒有子執行緒。執行緒物件是普通物件,run()方法是普通方法,run()方法一經呼叫立即執行,在執行完畢之前不會併發執行其它執行緒。補充一點:正常執行緒物件呼叫run()方法之後,若已處於執行狀態,則不能再呼叫start()方法了,否則會引發 IllegalThreadStateException異常。

           2.執行和阻塞狀態

              (1)如果處於就緒狀態的執行緒獲得了CPU,開始執行run()方法的執行緒執行體,則該執行緒處於執行狀態。

              (2)當一個執行緒執行後,它不可能一直處於執行狀態,執行緒在執行過程中需要被中斷,目的是為了給其它執行緒獲得執行的機會。這時被中斷的執行緒就處於阻塞態

              (3)當發生如下情況時,執行緒會進入阻塞狀態。

                     》執行緒呼叫sleep方法主動放棄所佔用的處理器資源。

                     》執行緒呼叫了一個阻塞式IO方法,在該方法返回之前,該執行緒被阻塞。

                     》執行緒試圖獲得一個同步監視器,但該同步監視器正在被其他執行緒所持有。(後面再寫吧。。)

                     》執行緒等待某個通知(notify)

                     》程式呼叫了執行緒的suspend()方法將該執行緒掛起。但這個方法容易死鎖,所以應該儘量避免使用該方法。  

               (4)當發生如下情況時,執行緒會解除上面的阻塞,重新進入執行狀態:

                     》呼叫sleep()方法的執行緒超過了指定時間。

                     》執行緒呼叫阻塞式IO方法已經返回。

                     》執行緒成功獲得了試圖取得的同步監視器。

                     》執行緒正在等待某個通知時,其他執行緒正好發出一個通知。

                     》處於掛起的狀態的執行緒被呼叫了resume()回覆方法。  

             3.執行緒死亡:

             執行緒會以如下三種方式結束:

                     》run()或call()方法執行完成,執行緒正常結束。

                     》執行緒丟擲一個未捕獲的異常或ERROR。

                     》直接呼叫該執行緒的stop方法來結束該執行緒-----該方法容易導致死鎖。   

四、執行緒的生命週期之狀態轉換

               

          1.初始狀態(新建)------>可執行狀態(就緒)
          2.執行狀態---------->終止狀態(死亡)

          3.執行狀態---------->可執行狀態(就緒)

                yield()方法:讓當前正在執行的執行緒暫停,但它不會阻塞該執行緒,只是讓執行緒轉為就緒狀態。系統排程器完全可以再重新對該就緒態的執行緒進行排程,但是要考慮到其它處於就緒狀態執行緒的優先順序,優先順序高的執行緒,才會獲得被執行的機會,從而進入執行狀態。

          4.執行狀態---------->阻塞狀態                (1)join()方法:目的是讓一個執行緒等待join進來的執行緒,只有當join進來的執行緒完全執行完畢,等待的執行緒才可以繼續執行。下面程式碼:
public class JoinThread extends Thread{
	//提供一個有引數構造器,用於設定該執行緒的名字
	public JoinThread(String name){
		super(name);
	}
	@Override
	public void run() {
		for (int i = 0; i < 100; i++) {
			System.out.println(getName()+"---"+i);
		}
	}
	
	public static void main(String[] args) throws Exception {
		
		for (int i = 0; i < 100; i++) {
			if(i==20){
				JoinThread jt = new JoinThread("join進來的執行緒");
				jt.start();
				jt.join();
			}
			System.out.println(Thread.currentThread().getName()+"==="+i);
		}
	}
}

main===16
main===17
main===18
main===19
join進來的執行緒---0
join進來的執行緒---1
。。。。。。。
。。。。。。。
join進來的執行緒---97
join進來的執行緒---98
join進來的執行緒---99
main===20
main===21
main===22
main===23
             (2)sleep()方法:讓當前正在執行的執行緒暫停,並進入阻塞狀態。其過載方法static void sleep(long millis)代表睡眠時間。
public class SleepThread extends Thread{
	public static void main(String[] args) throws Exception {
		for (int i = 0; i < 10; i++) {
			System.out.println("列印中"+i);
			if(i==5){
				sleep(3000);
			}
		}
	}
}
              當i==5時,列印輸出會停頓3秒,然後繼續列印。
            (3)IO阻塞:簡單的例子,使用Scanner物件,呼叫nextInt()方法等待使用者輸入數字,只有使用者輸入數字完成,程式才會繼續往下走。