Java基礎-併發篇

稷下 發表於 2022-06-24
Java

3.1. JAVA 併發知識庫

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-TW5HFaoE-1652355793598)(../images/31.png)]

3.2. JAVA 執行緒實現/建立方式

3.2.1. 繼承 Thread 類

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

public class MyThread extends Thread {
 public void run() {
 	System.out.println("MyThread.run()");
 }
    MyThread myThread1 = new MyThread();
	myThread1.start(); 
}

3.2.2. 實現 Runnable 介面。

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

public class MyThread extends OtherClass implements Runnable {
 	public void run() {
 		System.out.println("MyThread.run()");
 	}
} 
    //啟動 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();
 	}
}

3.2.3. ExecutorService、Callable、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());
}

3.2.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();
 	}
 }

3. 3.種執行緒池

​ Java 裡面執行緒池的頂級介面是 Executor,但是嚴格意義上講 Executor 並不是一個執行緒池,而 只是一個執行執行緒的工具。真正的執行緒池介面是 ExecutorService。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-RoX7OpuQ-1652355793599)(../images/32.png)]

3.3.1. newCachedThreadPool

​ 建立一個可根據需要建立新執行緒的執行緒池,但是在以前構造的執行緒可用時將重用它們。對於執行 很多短期非同步任務的程式而言,這些執行緒池通常可提高程式效能。呼叫 execute 將重用以前構造 的執行緒(如果執行緒可用)。如果現有執行緒沒有可用的,則建立一個新執行緒並新增到池中。終止並 從快取中移除那些已有 60 秒鐘未被使用的執行緒。因此,長時間保持空閒的執行緒池不會使用任何資 源。

3.3.2. newFixedThreadPool

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

3.3.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);

3.3.4. newSingleThreadExecutor

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

3.4. 執行緒生命週期(狀態)

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

3.4.1. 新建狀態(NEW)

​ 當程式使用 new 關鍵字建立了一個執行緒之後,該執行緒就處於新建狀態,此時僅由 JVM 為其分配 記憶體,並初始化其成員變數的值

3.4.2. 就緒狀態(RUNNABLE)

​ 當執行緒物件呼叫了 start()方法之後,該執行緒處於就緒狀態。Java 虛擬機器會為其建立方法呼叫棧和 程式計數器,等待排程執行。

3.4.3. 執行狀態(RUNNING)

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

3.4.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.4.5. 執行緒死亡(DEAD)

執行緒會以下面三種方式結束,結束後就是死亡狀態

正常結束

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

異常結束

​ 執行緒丟擲一個未捕獲的 Exception 或 Error。

呼叫 stop

​ 直接呼叫該執行緒的 stop()方法來結束該執行緒—該方法通常容易導致死鎖,不推薦使用。

3.5. 終止執行緒 4 種方式

3.5.1. 正常執行結束

​ 程式執行結束,執行緒自動結束。

3.5.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 的值。

3.5.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 跳出迴圈
 		}
 	}
 }

3.5.4. stop 方法終止執行緒(執行緒不安全)

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

3.6. sleep 與 wait 區別

​ 1. 對於 sleep()方法,我們首先要知道該方法是屬於 Thread 類中的。而 wait()方法,則是屬於 Object 類中的。

​ 2. sleep()方法導致了程式暫停執行指定的時間,讓出 cpu 該其他執行緒,但是他的監控狀態依然 保持者,當指定的時間到了又會自動恢復執行狀態。

​ 3.在呼叫 sleep()方法的過程中,執行緒不會釋放物件鎖。 4. 而當呼叫 wait()方法的時候,執行緒會放棄物件鎖,進入等待此物件的等待鎖定池,只有針對此 物件呼叫 notify()方法後本執行緒才進入物件鎖定池準備獲取物件鎖進入執行狀態。

​ 4.而當呼叫 wait()方法的時候,執行緒會放棄物件鎖,進入等待此物件的等待鎖定池,只有針對此 物件呼叫 notify()方法後本執行緒才進入物件鎖定池準備獲取物件鎖進入執行狀態。

3.7. start 與 run 區別

​ 1. start()方法來啟動執行緒,真正實現了多執行緒執行。這時無需等待 run 方法體程式碼執行完畢, 可以直接繼續執行下面的程式碼。

​ 2.通過呼叫 Thread 類的 start()方法來啟動一個執行緒, 這時此執行緒是處於就緒狀態, 並沒有運 行。

​ 3.方法 run()稱為執行緒體,它包含了要執行的這個執行緒的內容,執行緒就進入了執行狀態,開始運 行 run 函式當中的程式碼。 Run 方法執行結束, 此執行緒終止。然後 CPU 再排程其它執行緒。

3.8. 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 不會退出。