Java基礎知識回顧之五 ----- 多線程

前言

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

線程和進程

線程

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

進程

表示資源的分配和調度的一個獨立單元,一般表示爲執行中的程序。一個進程至少包含一個線程。bash

進程和線程的區別

  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。

在瞭解了線程和進程以後,咱們再來簡單的瞭解下單線程和多線程。 單線程 程序中只存在一個線程,實際上主方法就是一個主線程。dom

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

那麼在什麼狀況下用多線程呢?ui

通常來講,程序中有兩個以上的子系統須要併發執行的,這時候就須要利用多線程編程。經過對多線程的使用,能夠編寫出高效的程序。this

那麼是否是使用不少線程就能提升效率呢?spa

不必定的。由於程序中上下文的切換開銷也很重要,若是建立了太多的線程,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

相關文章
相關標籤/搜索