Java基礎知識回顧之五 ----- 多執行緒

虛無境發表於2018-05-28

前言

上一篇文章中,回顧了Java的集合。而在本篇文章中主要介紹多執行緒的相關知識。主要介紹的知識點為執行緒的介紹、多執行緒的使用、以及在多執行緒中使用的一些方法。

執行緒和程式

執行緒

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

程式

表示資源的分配和排程的一個獨立單元,通常表示為執行中的程式。一個程式至少包含一個執行緒。

程式和執行緒的區別

  1. 程式至少有一個執行緒;它們共享程式的地址空間;而程式有自己獨立的地址空間;
  2. 程式是資源分配和擁有的單位,而同一個程式內的執行緒共享程式的資源;
  3. 執行緒是處理器排程的基本單位,但程式不是;

生命週期

執行緒和程式一樣分為五個階段:建立就緒執行阻塞終止

  • 新建狀態:使用 new 關鍵字和 Thread 類或其子類建立一個執行緒物件後,該執行緒物件就處於新建狀態。它保持這個狀態直到程式start() 這個執行緒。
  • 就緒狀態:當執行緒物件呼叫了start()方法之後,該執行緒就進入就緒狀態。就緒狀態的執行緒處於就緒佇列中,要等待JVM裡執行緒排程器的排程。
  • 執行狀態:如果就緒狀態的執行緒獲取 CPU 資源,就可以執行 run(),此時執行緒便處於執行狀態。處於執行狀態的執行緒最為複雜,它可以變為阻塞狀態、就緒狀態和死亡狀態。
  • 阻塞狀態:如果一個執行緒執行了sleep(睡眠)、suspend(掛起)等方法,失去所佔用資源之後,該執行緒就從執行狀態進入阻塞狀態。在睡眠時間已到或獲得裝置資源後可以重新進入就緒狀態。可以分為三種:
  • 等待阻塞:執行狀態中的執行緒執行 wait() 方法,使執行緒進入到等待阻塞狀態。
  • 同步阻塞:執行緒在獲取 synchronized 同步鎖失敗(因為同步鎖被其他執行緒佔用)。
  • 其他阻塞:通過呼叫執行緒的 sleep() 或 join() 發出了 I/O 請求時,執行緒就會進入到阻塞狀態。當sleep() 狀態超時,join() 等待執行緒終止或超時,或者 I/O 處理完畢,執行緒重新轉入就緒狀態。
  • 死亡狀態:一個執行狀態的執行緒完成任務或者其他終止條件發生時,該執行緒就切換到終止狀態。

可以用下述圖來進行理解執行緒的生命週期:  

這裡寫圖片描述
注:上述圖來自http://www.runoob.com/wp-content/uploads/2014/01/java-thread.jpg。

在瞭解了執行緒和程式之後,我們再來簡單的瞭解下單執行緒和多執行緒。 單執行緒 程式中只存在一個執行緒,實際上主方法就是一個主執行緒。

多執行緒 多執行緒是指在同一程式中有多個順序流在執行。 簡單的說就是在一個程式中有多個任務執行。

那麼在什麼情況下用多執行緒呢?

一般來說,程式中有兩個以上的子系統需要併發執行的,這時候就需要利用多執行緒程式設計。通過對多執行緒的使用,可以編寫出高效的程式。

那麼是不是使用很多執行緒就能提高效率呢?

不一定的。因為程式中上下文的切換開銷也很重要,如果建立了太多的執行緒,CPU 花費在上下文的切換的時間將多於執行程式的時間!這時是會降低程式執行效率的。

所以有效利用多執行緒的關鍵是理解程式是併發執行而不是序列執行的。

執行緒的建立

一般來說,我們在對執行緒進行建立的時候,一般是繼承Thread 類或實現Runnable 介面。其實還有一種方式是實現 Callable介面,然後與Future 或執行緒池結合使用, 類似於Runnable介面,但是就功能上來說更為強大一些,也就是被執行之後,可以拿到返回值。

這裡我們分別一個例子使用繼承Thread 類、實現Runnable 介面和實現Callable介面與Future結合來進行建立執行緒。 程式碼示例: 注:執行緒啟動的方法是start而不是run。因為使用start方法整個執行緒處於就緒狀態,等待虛擬機器來進行排程。而使用run,也就是當作了一個普通的方法進行啟動,這樣虛擬機器不會進行執行緒排程,虛擬機器會執行這個方法直到結束後自動退出。

程式碼示例:

public class Test {
	public static void main(String[] args) {
		ThreadTest threadTest=new ThreadTest();
		threadTest.start();

		RunalbeTest runalbeTest=new RunalbeTest();
		Thread thread=new Thread(runalbeTest);
		thread.start();
		
		CallableTest callableTest=new CallableTest();
		FutureTask<Integer> ft = new FutureTask<Integer>(callableTest);  
		Thread thread2=new Thread(ft);
		thread2.start();
		try {
			System.out.println("返回值:"+ft.get());
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (ExecutionException e) {
			e.printStackTrace();
		}
	}
}

class ThreadTest extends Thread{
	 @Override
     public void run() {
        System.out.println("這是一個Thread的執行緒!");
    }
}

class RunalbeTest implements Runnable{
	 @Override
     public void run() {
        System.out.println("這是一個Runnable的執行緒!");
    }
}

class CallableTest implements Callable<Integer>{
	@Override
	public Integer call() throws Exception {
		 System.out.println("這是一個Callable的執行緒!");  
		return 2;
	}
}
複製程式碼

執行結果:

	這是一個Thread的執行緒!
	這是一個Runnable的執行緒!
	這是一個Callable的執行緒!
	返回值:2
複製程式碼

通過上述示例程式碼中,我們發現使用繼承 Thread 類的方式建立執行緒時,編寫最為簡單。而使用Runnable、Callable 介面的方式建立執行緒的時候,需要通過Thread類的構造方法Thread(Runnable target) 構造出物件,然後呼叫start方法來執行執行緒程式碼。順便說下,其實Thread類實際上也是實現了Runnable介面的一個類。

但是在這裡,我推薦大家建立單執行緒的時候使用繼承 Thread 類方式建立,多線執行緒的時候使用Runnable、Callable 介面的方式來建立建立執行緒。 至於為什麼呢?在下面中的描述已給出理由。

  • 繼承 Thread 類建立的執行緒,可以直接使用Thread類中的方法,比如休眠直接就可以使用sleep方法,而不必在前面加個Thread;獲取當前執行緒Id,只需呼叫getId就行,而不必使用Thread.currentThread().getId() 這麼一長串的程式碼。但是使用Thread 類建立的執行緒,也有其侷限性。比如資源不能共享,無法放入執行緒池中等等。
  • 使用Runnable、Callable 介面的方式建立的執行緒,可以實現資源共享,增強程式碼的複用性,並且可以避免單繼承的侷限性,可以和執行緒池完美結合。但是也有不好的,就是寫起來不太方便,使用其中的方法不夠簡介。

總的來說就是,單執行緒建議用繼承 Thread 類建立,多執行緒建議- 使用Runnable、Callable 介面的方式建立。

執行緒的一些常用方法

yield

使用yield方法表示暫停當前正在執行的執行緒物件,並執行其他執行緒。

程式碼示例:

public class YieldTest {
	public static void main(String[] args) {
		Test1 t1 = new Test1("張三");
		Test1 t2 = new Test1("李四");
		new Thread(t1).start();
		new Thread(t2).start();
	}
}

class Test1 implements Runnable {
	private String name;
	public Test1(String name) {
		this.name=name;
	}
	@Override
	public void run() {
        System.out.println(this.name + " 執行緒執行開始!");  
		for (int i = 1; i <= 5; i++) {
            System.out.println(""+this.name + "-----" + i);  
			// 當為3的時候,讓出資源
			if (i == 3) {
				Thread.yield();
			}
		}
        System.out.println(this.name + " 執行緒執行結束!");  
	}
}
複製程式碼

執行結果一:

	張三 執行緒執行開始!
	張三-----1
	張三-----2
	張三-----3
	李四 執行緒執行開始!
	李四-----1
	李四-----2
	李四-----3
	張三-----4
	張三-----5
	張三 執行緒執行結束!
	李四-----4
	李四-----5
	李四 執行緒執行結束!
複製程式碼

執行結果二:

張三 執行緒執行開始!
李四 執行緒執行開始!
李四-----1
李四-----2
李四-----3
張三-----1
張三-----2
張三-----3
李四-----4
李四-----5
李四 執行緒執行結束!
張三-----4
張三-----5
張三 執行緒執行結束!
複製程式碼

上述中的例子我們可以看到,啟動兩個執行緒之後,哪個執行緒先執行到3,就會讓出資源,讓另一個執行緒執行。 在這裡順便說下,yieldsleep的區別。

  • yield: yield只是使當前執行緒重新回到可執行狀態,所以執行yield()的執行緒有可能在進入到可執行狀態後馬上又被執行。
  • sleep:sleep使當前執行緒進入停滯狀態,所以執行sleep()的執行緒在指定的時間內肯定不會被執行;

join

使用join方法指等待某個執行緒終止。也就是說當子執行緒呼叫了join方法之後,後面的程式碼只有等待該執行緒執行完畢之後才會執行。

如果不好理解,這裡依舊使用一段程式碼來進行說明。 這裡我們建立兩個執行緒,並使用main方法執行。順便提一下,其實main方法也是個執行緒。如果直接執行的話,可能main方法執行完畢了,子執行緒還沒執行完畢,這裡我們就讓子執行緒使用join方法使main方法最後執行。

程式碼示例:

public class JoinTest {
	public static void main(String[] args) {
		 System.out.println(Thread.currentThread().getName()+ "主執行緒開始執行!");  
		 Test2 t1=new Test2("A");  
		 Test2 t2=new Test2("B");  
		 t1.start();  
	     t2.start();  
	      try {  
	    	  t1.join();  
	        } catch (InterruptedException e) {  
	            e.printStackTrace();  
	        }  
	        try {  
	        	t2.join();  
	        } catch (InterruptedException e) {  
	            e.printStackTrace();  
	        }    
	     System.out.println(Thread.currentThread().getName()+ "主執行緒執行結束!");  
	}

}

class Test2 extends Thread{  
    public Test2(String name) {  
        super(name);  
    }  
    public void run() {  
         System.out.println(this.getName() + " 執行緒執行開始!");  
       for (int i = 0; i < 5; i++) {  
           System.out.println("子執行緒"+this.getName() + "執行 : " + i);  
           try {  
               sleep(new Random().nextInt(10));  
           } catch (InterruptedException e) {  
               e.printStackTrace();  
           }  
       }  
       System.out.println(this.getName() + " 執行緒執行結束!");  
   }
}
複製程式碼

執行結果:

	main主執行緒開始執行!
	B 執行緒執行開始!
	子執行緒B執行 : 0
	A 執行緒執行開始!
	子執行緒A執行 : 0
	子執行緒A執行 : 1
	子執行緒B執行 : 1
	子執行緒B執行 : 2
	子執行緒B執行 : 3
	子執行緒B執行 : 4
	B 執行緒執行結束!
	子執行緒A執行 : 2
	子執行緒A執行 : 3
	子執行緒A執行 : 4
	A 執行緒執行結束!
	main主執行緒執行結束!
複製程式碼

上述示例中的結果顯然符合我們的預期。

priority

使用setPriority表示設定執行緒的優先順序。 每個執行緒都有預設的優先順序。主執行緒的預設優先順序為Thread.NORM_PRIORITY。 執行緒的優先順序有繼承關係,比如A執行緒中建立了B執行緒,那麼B將和A具有相同的優先順序。 JVM提供了10個執行緒優先順序,但與常見的作業系統都不能很好的對映。如果希望程式能移植到各個作業系統中,應該僅僅使用Thread類有以下三個靜態常量作為優先順序,這樣能保證同樣的優先順序採用了同樣的排程方式

  • static int MAX_PRIORITY 執行緒可以具有的最高優先順序,取值為10。
  • static int MIN_PRIORITY 執行緒可以具有的最低優先順序,取值為1。
  • static int NORM_PRIORITY 分配給執行緒的預設優先順序,取值為5。

但是設定優先順序並不能保證執行緒一定先執行。我們可以通過一下程式碼來驗證。

程式碼示例:

public class PriorityTest {
  public static void main(String[] args) {
		Test3 t1 = new Test3("張三");
		Test3 t2 = new Test3("李四");
		t1.setPriority(Thread.MIN_PRIORITY);
		t2.setPriority(Thread.MAX_PRIORITY);
		t1.start();
		t2.start();
	}
}

class Test3 extends Thread {
	public Test3(String name) {
		super(name);
	}
	@Override
	public void run() {
        System.out.println(this.getName() + " 執行緒執行開始!");  
		for (int i = 1; i <= 5; i++) {
            System.out.println("子執行緒"+this.getName() + "執行 : " + i); 
            try {  
                sleep(new Random().nextInt(10));  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            } 
		}
        System.out.println(this.getName() + " 執行緒執行結束!");  
	}
}
複製程式碼

執行結果一:

李四 執行緒執行開始!
子執行緒李四執行 : 1
張三 執行緒執行開始!
子執行緒張三執行 : 1
子執行緒張三執行 : 2
子執行緒李四執行 : 2
子執行緒李四執行 : 3
子執行緒李四執行 : 4
子執行緒張三執行 : 3
子執行緒李四執行 : 5
李四 執行緒執行結束!
子執行緒張三執行 : 4
子執行緒張三執行 : 5
張三 執行緒執行結束!
複製程式碼

執行結果二:

張三 執行緒執行開始!
子執行緒張三執行 : 1
李四 執行緒執行開始!
子執行緒李四執行 : 1
子執行緒張三執行 : 2
子執行緒張三執行 : 3
子執行緒李四執行 : 2
子執行緒張三執行 : 4
子執行緒李四執行 : 3
子執行緒張三執行 : 5
子執行緒李四執行 : 4
張三 執行緒執行結束!
子執行緒李四執行 : 5
李四 執行緒執行結束!
複製程式碼

執行結果三:

李四 執行緒執行開始!
子執行緒李四執行 : 1
張三 執行緒執行開始!
子執行緒張三執行 : 1
子執行緒李四執行 : 2
子執行緒李四執行 : 3
子執行緒李四執行 : 4
子執行緒張三執行 : 2
子執行緒張三執行 : 3
子執行緒張三執行 : 4
子執行緒李四執行 : 5
子執行緒張三執行 : 5
李四 執行緒執行結束!
張三 執行緒執行結束!
複製程式碼

執行緒中一些常用的方法

執行緒中還有許多方法,但是這裡並不會全部細說。只簡單的列舉了幾個方法使用。更多的方法使用可以檢視相關的API文件。這裡我也順便總結了一些關於這些方法的描述。

  1. sleep:在指定的毫秒數內讓當前正在執行的執行緒休眠(暫停執行);不會釋放物件鎖。
  2. join:指等待t執行緒終止。
  3. yield:暫停當前正在執行的執行緒物件,並執行其他執行緒。
  4. setPriority:設定一個執行緒的優先順序。
  5. interrupt:一個執行緒是否為守護執行緒。
  6. wait:強迫一個執行緒等待。它是Object的方法,也常常和sleep作為比較。需要注意的是wait會釋放物件鎖,讓其它的執行緒可以訪問;使用wait必須要進行異常捕獲,並且要對當前所呼叫,即必須採用synchronized中的物件。
  7. isAlive: 判斷一個執行緒是否存活。
  8. activeCount: 程式中活躍的執行緒數。
  9. enumerate: 列舉程式中的執行緒。
  10. currentThread: 得到當前執行緒。
  11. setDaemon: 設定一個執行緒為守護執行緒。(使用者執行緒和守護執行緒的區別在於,是否等待主執行緒依賴於主執行緒結束而結束)。
  12. setName: 為執行緒設定一個名稱。
  13. notify(): 通知一個執行緒繼續執行。它也是Object的一個方法,經常和wait方法一起使用。

結語

其實這篇文章很久之前都已經打好草稿了,但是由於各種原因,只到今天才寫完。雖然也只是簡單的介紹了一下多執行緒的相關知識,也只能算個入門級的教程吧。不過寫完之後,感覺自己又重新複習了一遍多執行緒,對多執行緒的理解又加深了一些。 話已盡此,不在多說。 原創不易,如果感覺不錯,希望給個推薦!您的支援是我寫作的最大動力!

參考:https://blog.csdn.net/evankaka/article/details/44153709#t1

版權宣告: 作者:虛無境 部落格園出處:http://www.cnblogs.com/xuwujing CSDN出處:http://blog.csdn.net/qazwsxpcm     個人部落格出處:http://www.panchengming.com

相關文章