3.1. JAVA 併發知識庫
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 不會退出。