《Java 編程思想》讀書筆記之並發(二)
基本的線程機制
並發編程使我們可以將程序劃分為多個分離的、獨立運行的任務。通過使用多線程機制,這些獨立的任務(也被稱為子任務)中的每一個都將由「執行線程」來驅動。一個線程就是在進程中的一個單一的順序控制流。
在使用線程時,CPU 將輪流給每個任務分配其占用時間。每個任務都覺得自己在一直占用 CPU,但事實上 CPU 時間是劃分成片段分配給了所有任務(此為單 CPU 情況,多 CPU 確實是同時執行)。
使用線程機制是一種建立透明的、可擴展的程序的方法,如果程序運行得太慢,為機器增添一個 CPU 就能很容易地加快程序的運行速度。多任務和多線程往往是使用多處理器系統的最合理的方式。大數據的分布式擴展思想與之類似,當程序性能不行時,可以通過擴展集群提高程序並發度提高性能,但是不許修改代碼。
定義任務
線程可以驅動任務,而描述任務需要一定的方式,java 中建議的方式是實現 Runnable 接口,其次是繼承 Thread 類。以下是兩種方式的代碼實現:
Runnable 接口實現
public class MyTask implements Runnable { @Override public void run() { System.out.println(Thread.currentThread() + ": running..."); } public static void main(String[] args) { Thread t = new Thread(new MyTask()); t.start(); System.out.println(Thread.currentThread() + ": running..."); } }
Thread 繼承實現
public class MyThread extends Thread { @Override public void run() { System.out.println(Thread.currentThread() + ": running..."); } public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); System.out.println(Thread.currentThread() + ": running..."); } }
使用 Executor
Java SE5 的 java.util.concurrent 包中的執行器(Executor)將為你管理 Thread 對象,從而簡化了並發編程。其實就是 Java 的線程池,它大大的減少的對於線程的管理,包括線程的創建、銷毀等,並且它還能復用已經創建的線程對象,減少由於反復創建線程引起的開銷,即節省了資源,同時也提高了程序的運行效率。這部分內容比較重要,之後會單獨開一篇介紹,此處就介紹到這。
從任務中產生返回值
實現 Runnable 接口只能執行任務,無法獲得任務的返回值。如果希望獲得返回值,則應該實現 Callable 接口,並且應該使用 ExecutorService.submit() 方法調用它。下面是一個示例:
public class MyCallableTask implements Callable<String> {
private int id;
public MyCallableTask(int id) {
this.id = id;
}
@Override
public String call() throws Exception {
return "Result : " + Thread.currentThread() + ": " + id;
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
List<Future<String>> futures = new ArrayList<>();
for (int i = 0; i < 10; i++) {
futures.add(exec.submit(new MyCallableTask(i)));
}
for (Future<String> future : futures) {
try {
// 調用 future 方法會導致線程阻塞,直到 future 對應的線程執行完畢
System.out.println(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} finally {
exec.shutdown();
}
}
}
}
submit() 方法會產生 Future 對象,並且使用泛型的方式對返回值類型進行了定義。調用 Future.get() 方法,會導致線程阻塞,直到被調用的線程執行完畢返回結果,當然 java 也提供了設置超時時間的 get() 方法,防止線程一直阻塞,或者你也可以調用 Future 的 isDone() 方法預先判斷線程是否執行完畢,再調用 get() 獲取返回值。
休眠
「休眠」就是使任務中止執行指定的時間。在 Java 中可以通過Thread.sleep()
方法來實現,JDK 1.6 之後推薦使用TimeUnit
來實現任務的休眠。
對sleep()
方法的調用會拋出InterruptedException
異常,由於異常不能跨線程傳播,因此必須在本地處理任務內部產生的異常。
線程間雖然可以切換,但是並沒有固定的順序可言,因此,若要控制任務執行的順序,絕對不要寄希望於線程的調度機制。
優先級
線程的優先級是用來控制線程的執行頻率的,優先級高的線程執行頻率高,但這並不會導致優先級低的線程得不到執行,僅僅是降低執行的頻率。
在絕大多數時間裏,所有線程都應該以默認的優先級運行。試圖操縱線程優先級通常是一種錯誤。——《Java 編程思想》
你可以在一個任務的內部,通過調用Thread.currentThread()
來獲得對驅動該任務的 Thread 對象的引用。
盡管 JDK 有 10 個優先級,但它與多數操作系統都不能映射得很好。因此在手動指定線程優先級的時候盡量只使用MAX_PRIORITY
,NORM_PRIORITY
,MIN_PRIORITY
三種級別。
讓步
通過調用Thread.yield()
方法可以使當前線程主動讓出 CPU,同時向系統建議具有「相同優先級」的其他線程可以運行(只是一個建議,沒有任何機制保證它一定會被采納)。因此,對於任何重要的控制或在調整應用時,都不能依賴於yield()
。
後臺線程
所謂後臺(daemon)線程,是指在程序運行的時候在後臺提供一種通用服務的線程,並且這種線程並不屬於程序中不可或缺的部分。——《Java 編程思想》
因此,當所有的非後臺線程結束時,程序也就中止了,同時會殺死進程中的所有後臺線程。
必須在線程啟動之前調用setDeamon()
方法,才能把它設置為後臺線程。
可以通過調用isDaemon()
方法來確定線程是否是一個後臺線程。如果是一個後臺線程,那麽它創建的任何線程將被自動設置成後臺線程。
如果在後臺線程中有finally{}
語句塊,當所有非後臺程序執行結束時,後臺線程會突然終止,並不會執行finally{}
語句塊中的內容,後臺線程不會有任何開發者希望出現的結束確認形式。因為不能以優雅的方式來關閉後臺線程,所以它們幾乎不是一種好的思想。
加入一個線程
如果某個線程在另一個線程 t 上調用t.join()
,此線程將被掛起,直到目標線程 t 結束才恢復(即t.isAlive()
返回為 false)。也可以在調用join()
時帶上一個超時參數,在目標線程處理超時時join()
方法總能返回。
對join()
方法的調用可以被中斷,只要在調用線程上調用interrupt()
方法。
捕獲異常
由於線程的本質特性,一旦在run()
方法中未捕捉異常,那麽異常就會向外傳播到控制臺,除非采取特殊的步驟捕獲這種錯誤的異常。
在 Java SE5 之前,可以使用線程組來捕獲這些異常(不推薦),在 Java SE5 之後可以用 Executor 來解決這個問題。Executor 允許修改產生線程的方式,允許你在每個 Thread 對象上都附著一個異常處理器Thread.UncaughtExceptionHandler
,此異常處理器會在線程因未捕獲的異常而臨近死亡時被調用uncaughtException()
方法處理未捕獲的異常。這通常是在一組線程以同樣方式處理未捕獲異常時使用,若不同的線程需要有不同的異常處理方式,則最好在線程內部單獨處理。
《Java 編程思想》讀書筆記之並發(二)