Java多線程基礎

Java多線程基礎

1、Java線程實現/創建方式

1.1 繼承Thread類

Thread類本質上是實現了Runnable接口的一個實例,代表一個線程的實例。啓動線程的唯一方法就是通過Thread類的start()實例方法。start()方法是一個native方法,它將啓動一個新線程,並執行run()方法。

class MyThread extends Thread { 
    public void run() { 
        System.out.println("MyThread.run()"); 
    } 
} 
public class Test{
    public static void main(String[] args){
     	MyThread myThread1 = new MyThread();
		myThread1.start();   
    }
}

1.2 實現Runnable接口

如果自己的類已經extends另一個類,就無法直接extends Thread,此時,可以實現一個Runnable接口。

public class MyThread extends OtherClass implements Runnable { 
    public void run() { 
        System.out.println("MyThread.run()"); 
    } 
}
public class Test{
    public static void main(String[] args){
     	//啓動MyThread,需要首先實例化一個Thread,並傳入自己的MyThread實例: 
		MyThread myThread = new MyThread(); 
		Thread thread = new Thread(myThread); 
		thread.start(); 
		//事實上,當傳入一個Runnable target參數給Thread後,Thread的run()方法就會調用target.run() 
		/*public void run() { 
    		if (target != null) { 
        		target.run(); 
    		} 
		}*/
    }
}

1.3 ExecutorService、Callable<Class>、Future有返回值線程

有返回值的任務必須實現Callable接口,類似的,無返回值的任務必須Runnable接口。執行Callable任務後,可以獲取一個Future的對象,在該對象上調用get就可以獲取到Callable任務返回的Object了,再結合線程池接口ExecutorService就可以實現傳說中有返回結果的多線程了。

//創建一個線程池 
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); 
    list.add(f); 
} 

// 關閉線程池 
pool.shutdown(); 

// 獲取所有併發任務的運行結果 
for (Future f : list) { 
    // 從Future對象上獲取任務的返回值,並輸出到控制檯 
    System.out.println("res:" + f.get().toString()); 
}

1.4 基於線程池的方式

線程和數據庫連接這些資源都是非常寶貴的資源。那麼每次需要的時候創建,不需要的時候銷燬,是非常浪費資源的。那麼我們就可以使用緩存的策略,也就是使用線程池。

// 創建線程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
while(true) {
	threadPool.execute(new Runnable() { 
         // 提交多個線程任務,並執行
		@Override
		public void run() {
			System.out.println(Thread.currentThread().getName() + " is running ..");
			try {
				Thread.sleep(3000);
			} catch (InterruptedException e) {
				e.printStackTrace();				}
			}
    	}
	});
}

2、四種線程池

Java裏面線程池的頂級接口是Executor,但是嚴格意義上講Executor並不是一個線程池,而只是一個執行線程的工具。真正的線程池接口是ExecutorService。

在這裏插入圖片描述

2.1 newCachedThreadPool

創建一個可根據需要創建新線程的線程池,但是在以前構造的線程可用時將重用它們。對於執行很多短期異步任務的程序而言,這些線程池通常可提高程序性能。調用 execute 將重用以前構造的線程(如果線程可用)。如果現有線程沒有可用的,則創建一個新線程並添加到池中。終止並從緩存中移除那些已有 60 秒鐘未被使用的線程。因此,長時間保持空閒的線程池不會使用任何資源。

2.2 newFixedThreadPool

創建一個可重用固定線程數的線程池,以共享的無界隊列方式來運行這些線程。在任意點,在大多數 nThreads 線程會處於處理任務的活動狀態。如果在所有線程處於活動狀態時提交附加任務,則在有可用線程之前,附加任務將在隊列中等待。如果在關閉前的執行期間由於失敗而導致任何線程終止,那麼一個新線程將代替它執行後續的任務(如果需要)。在某個線程被顯式地關閉之前,池中的線程將一直存在。

2.3 newScheduledThreadPool

創建一個線程池,它可安排在給定延遲後運行命令或者定期地執行。

ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);
scheduledThreadPool.schedule(newRunnable(){
	@Override
	public void run() {
		System.out.println("延遲三秒");
	}
}, 3, TimeUnit.SECONDS);

scheduledThreadPool.scheduleAtFixedRate(newRunnable(){
	@Override
	public void run() {
		System.out.println("延遲 1 秒後每三秒執行一次");
	}
},1,3,TimeUnit.SECONDS);

2.4 newSingleThreadExecutor

Executors.newSingleThreadExecutor()返回一個線程池(這個線程池只有一個線程),這個線程池可以在線程死後(或發生異常時)重新啓動一個線程來替代原來的線程繼續執行下去!

3、線程的生命週期(狀態)

當線程被創建並啓動以後,它既不是一啓動就進入了執行狀態,也不是一直處於執行狀態。在線程的生命週期中,它要經過新建(New)、就緒(Runnable)、運行(Running)、阻塞(Blocked)和死亡(Dead)5種狀態。尤其是當線程啓動以後,它不可能一直"霸佔"着CPU獨自運行,所以CPU需要在多條線程之間切換,於是線程狀態也會多次在運行、阻塞之間切換。

3.1 新建狀態(NEW)

當程序使用new關鍵字創建了一個線程之後,該線程就處於新建狀態,此時僅由JVM爲其分配內存,並初始化其成員變量的值。

3.2 就緒狀態(RUNNABLE)

當線程對象調用了start()方法之後,該線程處於就緒狀態。Java虛擬機會爲其創建方法調用棧和程序計數器,等待調度運行。

3.3 運行狀態(RUNNING)

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

3.4 阻塞狀態(BLOCKED)

阻塞狀態是指線程因爲某種原因放棄了cpu 使用權,也即讓出了cpu timeslice,暫時停止運行。直到線程進入可運行(runnable)狀態,纔有機會再次獲得cpu timeslice 轉到運行(running)狀態。阻塞的情況分三種:

  • 等待阻塞(o.wait->等待對列):

    ​ 運行(running)的線程執行o.wait()方法,JVM會把該線程放入等待隊列(waitting queue)中。

  • 同步阻塞(lock->鎖池)

    ​ 運行(running)的線程在獲取對象的同步鎖時,若該同步鎖被別的線程佔用,則JVM會把該線程放入鎖池(lock pool)中。

  • 其他阻塞(sleep/join)

    ​ 運行(running)的線程執行Thread.sleep(long ms)或t.join()方法,或者發出了I/O請求時,JVM會把該線程置爲阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入可運行(runnable)狀態。

3.5 線程死亡(DEAD)

線程會以下面三種方式結束,結束後就是死亡狀態。

  • 正常結束

    ​ run()或call()方法執行完成,線程正常結束。

  • 異常結束

    ​ 線程拋出一個未捕獲的Exception或Error。

  • 調用stop

    ​ 直接調用該線程的stop()方法來結束該線程—該方法通常容易導致死鎖,不推薦使用。

在這裏插入圖片描述

4、終止線程4種方式

4.1 正常運行結束

程序運行結束,線程自動結束。

4.2 使用退出標誌退出線程

一般run()方法執行完,線程就會正常結束,然而,常常有些線程是伺服線程。它們需要長時間的運行,只有在外部某些條件滿足的情況下,才能關閉這些線程。使用一個變量來控制循環,例如:最直接的方法就是設一個boolean類型的標誌,並通過設置這個標誌爲true或false來控制while循環是否退出,代碼示例:

public class ThreadSafe extends Thread { 
    public volatile boolean exit = false; 
    public void run() { 
        while (!exit){ 
            //do something 
        } 
    } 
}

定義了一個退出標誌exit,當exit爲true時,while循環退出,exit的默認值爲false.在定義exit時,使用了一個Java關鍵字volatile,這個關鍵字的目的是使exit同步,也就是說在同一時刻只能由一個線程來修改exit的值。

4.3 Interrupt方法結束線程

使用interrupt()方法來中斷線程有兩種情況:

  1. 線程處於阻塞狀態:如使用了sleep,同步鎖的wait,socket中的receiver,accept等方法時,會使線程處於阻塞狀態。當調用線程的interrupt()方法時,會拋出InterruptException異常。阻塞中的那個方法拋出這個異常,通過代碼捕獲該異常,然後break跳出循環狀態,從而讓我們有機會結束這個線程的執行。通常很多人認爲只要調用interrupt方法線程就會結束,實際上是錯的, 一定要先捕獲InterruptedException異常之後通過break來跳出循環,才能正常結束run方法。
  2. 線程未處於阻塞狀態:使用isInterrupted()判斷線程的中斷標誌來退出循環。當使用interrupt()方法時,中斷標誌就會置true,和使用自定義的標誌來控制循環是一樣的道理。
public class ThreadSafe extends Thread {
	public void run() {
		while (!isInterrupted()){ 
            //非阻塞過程中通過判斷中斷標誌來退出
			try{
				Thread.sleep(5*1000);//阻塞過程捕獲中斷異常來退出
			}catch(InterruptedException e){
				e.printStackTrace();
				break;//捕獲到異常之後,執行break跳出循環
			}
		}
	}
}

4.4 stop方法終止線程(線程不安全)

程序中可以直接使用thread.stop()來強行終止線程,但是stop方法是很危險的,就象突然關閉計算機電源,而不是按正常程序關機一樣,可能會產生不可預料的結果,不安全主要是:thread.stop()調用之後,創建子線程的線程就會拋出ThreadDeatherror的錯誤,並且會釋放子線程所持有的所有鎖。一般任何進行加鎖的代碼塊,都是爲了保護數據的一致性,如果在調用thread.stop()後導致了該線程所持有的所有鎖的突然釋放(不可控制),那麼被保護數據就有可能呈現不一致性,其他線程在使用這些被破壞的數據時,有可能導致一些很奇怪的應用程序錯誤。因此,並不推薦使用stop方法來終止線程。

5、sleep與wait 區別

  1. 對於sleep()方法,我們首先要知道該方法是屬於Thread類中的。而wait()方法,則是屬於Object類中的。
  2. sleep()方法導致了程序暫停執行指定的時間,讓出cpu該其他線程,但是他的監控狀態依然保持者,當指定的時間到了又會自動恢復運行狀態。
  3. 在調用sleep()方法的過程中,線程不會釋放對象鎖。
  4. 而當調用wait()方法的時候,線程會放棄對象鎖,進入等待此對象的等待鎖定池,只有針對此對象調用notify()方法後本線程才進入對象鎖定池準備獲取對象鎖進入運行狀態。

6、start與run區別

  1. start()方法來啓動線程,真正實現了多線程運行。這時無需等待run方法體代碼執行完畢,可以直接繼續執行下面的代碼。
  2. 通過調用Thread類的start()方法來啓動一個線程, 這時此線程是處於就緒狀態, 並沒有運行。
  3. 方法run()稱爲線程體,它包含了要執行的這個線程的內容,線程就進入了運行狀態,開始運行run函數當中的代碼。 Run方法運行結束, 此線程終止。然後CPU再調度其它線程。

7、JAVA後臺線程

  1. 定義:守護線程–也稱“服務線程”,他是後臺線程,它有一個特性,即爲用戶線程 提供 公共服務,在沒有用戶線程可服務時會自動離開。
  2. 優先級:守護線程的優先級比較低,用於爲系統中的其它對象和線程提供服務。
  3. 設置:通過setDaemon(true)來設置線程爲“守護線程”;將一個用戶線程設置爲守護線程的方式是在 線程對象創建 之前 用線程對象的setDaemon方法。
  4. 在Daemon線程中產生的新線程也是Daemon的。
  5. 線程則是JVM級別的,以Tomcat 爲例,如果你在Web 應用中啓動一個線程,這個線程的生命週期並不會和Web應用程序保持同步。也就是說,即使你停止了Web應用,這個線程依舊是活躍的。
  6. example: 垃圾回收線程就是一個經典的守護線程,當我們的程序中不再有任何運行的Thread,程序就不會再產生垃圾,垃圾回收器也就無事可做,所以當垃圾回收線程是JVM上僅剩的線程時,垃圾回收線程會自動離開。它始終在低級別的狀態中運行,用於實時監控和管理系統中的可回收資源。
  7. 生命週期:守護進程(Daemon)是運行在後臺的一種特殊進程。它獨立於控制終端並且週期性地執行某種任務或等待處理某些發生的事件。也就是說守護線程不依賴於終端,但是依賴於系統,與系統“同生共死”。當JVM中所有的線程都是守護線程的時候,JVM就可以退出了;如果還有一個或以上的非守護線程則JVM不會退出。

參考

《Java核心知識點》

發佈了33 篇原創文章 · 獲贊 69 · 訪問量 1616
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章