能有此文十分感謝《Java編程思想》一書及其作者Bruce Eckel!
七、讓步
如果知道已經完成了在run()方法的循環的一次迭代過程中所需的工作,就可以給線程調度機制一個暗示:工作已經做得差不多了,可以讓別的線程使用CPU了。這個暗示將通過調用yield()方法來作出(不過這只是一個暗示,沒有任何機制保證它將會被採納)。當調用yield()時,你也是在建議具有相同優先級的其他線程可以運行。
八、後臺線程
所謂後臺線程,是指在程序運行時在後臺提供一種通用服務的線程,並且這種線程並不屬於程序中不可或缺的部分。因此,當所有的非後臺線程結束時,程序也就終止了,同時會殺死進程中所有的後臺線程。反過來說,只要有任何非後臺線程還在運行,程序就不會終止。
import java.util.concurrent.*;
public class SimpleDaemons implements Runnable {
public void run() {
try {
while(true) {
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(Thread.currentThread() + " " + this);
}
} catch(InterruptedException e) {
System.out.println("sleep() interrupted");
}
}
public static void main(String[] args) throws Exception {
for(int i = 0; i < 10; i++) {
Thread daemon = new Thread(new SimpleDaemons());
daemon.setDaemon(true); // Must call before start()
daemon.start();
}
System.out.println("All daemons started");
TimeUnit.MILLISECONDS.sleep(175);
}
}
必須在線程啓動之前調用setDaemon()方法,才能把它設置爲後臺線程。
一旦main()完成其工作,就沒什麼能阻止程序終止了,因爲除了後臺線程之外,已經沒有線程在運行了。main()線程被設定爲短暫睡眠,所以可以觀察到所有後臺線程啓動後的結果。
SimpleDaemons.java創建了顯式的線程,以便可以設置它們的後臺標誌。通過編寫定製的ThreadFactory可以定製由Executor創建的線程的屬性(後臺、優先級、名稱):
import java.util.concurrent.ThreadFactory;
public class DaemonThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
return t;
}
}
與普通的ThreadFactory的唯一差異就是它將後臺狀態全部設置爲True。下面可以用這個新的DaemonThreadFactory 作爲參數傳遞給Executor.newCachedThreadPool():
import java.util.concurrent.*;
public class DaemonFromFactory implements Runnable {
public void run() {
try {
while(true) {
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(Thread.currentThread() + " " + this);
}
} catch(InterruptedException e) {
System.out.println("Interrupted");
}
}
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool(
new DaemonThreadFactory());
for(int i = 0; i < 10; i++)
exec.execute(new DaemonFromFactory());
System.out.println("All daemons started");
TimeUnit.MILLISECONDS.sleep(500); // Run for a while
}
}
可以通過調用isDaemon()方法來確定線程是否是一個後臺線程。如果是一個後臺線程,那麼它創建的任何線程將被自動設置爲後臺線程。
後臺線程在不執行finally子句的情況下就會終止其run()方法:
import java.util.concurrent.*;
class ADaemon implements Runnable {
public void run() {
try {
System.out.println("Starting ADaemon");
TimeUnit.SECONDS.sleep(1);
} catch(InterruptedException e) {
System.out.println("Exiting via InterruptedException");
} finally {
System.out.println("This should always run?");
}
}
}
public class DaemonsDontRunFinally {
public static void main(String[] args) throws Exception {
Thread t = new Thread(new ADaemon());
t.setDaemon(true);
t.start();
}
}
當運行這個程序時候將看到finally子句並不會執行。但是如果註釋掉對t.setDaemon(true);調用,就會看到finally子句將會執行。
九、實現線程的幾種代碼編寫方式
到目前爲止,以上示例中,任務類都實現了Runnable。在一些簡單的情況下使用
直接從Thread繼承這種可替換的方式,如下:
public class SimpleThread extends Thread {
private int countDown = 5;
private static int threadCount = 0;
public SimpleThread() {
// Store the thread name:
super(Integer.toString(++threadCount));
start();
}
public String toString() {
return "#" + getName() + "(" + countDown + "), ";
}
public void run() {
while(true) {
System.out.print(this);
if(--countDown == 0)
return;
}
}
public static void main(String[] args) {
for(int i = 0; i < 5; i++)
new SimpleThread();
}
}
可以通過調用適當的Thread構造器爲Thread對象賦予具體的名稱,這個名稱可以通過使用getName()從toString()中獲得。
另外一種可能會看到的慣用法是自管理的Runnable:
public class SelfManaged implements Runnable {
private int countDown = 5;
private Thread t = new Thread(this);
public SelfManaged() { t.start(); }
public String toString() {
return Thread.currentThread().getName() +
"(" + countDown + "), ";
}
public void run() {
while(true) {
System.out.print(this);
if(--countDown == 0)
return;
}
}
public static void main(String[] args) {
for(int i = 0; i < 5; i++)
new SelfManaged();
}
}
這與從Thread繼承並沒有什麼特別差異,只是語法稍晦澀一點。但是,實現接口使得可以繼承另外一個不同的類。
注意,start()是在構造器中調用的。這個示例相當簡單,因此可能是安全的,但是應該意識到,在構造器中啓動線程可能會變得很有問題,因爲另外一個任務可能會在構造器結束之前執行,這就意味着該任務能夠訪問處於不穩定狀態的對象。這是優選Executor而不是顯式創建Thread對象的另一個原因。
有時通過內部類來將線程代碼隱藏在類中將會很有用,如下:
import java.util.concurrent.*;
// Using a named inner class:
class InnerThread1 {
private int countDown = 5;
private Inner inner;
private class Inner extends Thread {
Inner(String name) {
super(name);
start();
}
public void run() {
try {
while(true) {
System.out.println(this);
if(--countDown == 0) return;
sleep(10);
}
} catch(InterruptedException e) {
System.out.println("interrupted");
}
}
public String toString() {
return getName() + ": " + countDown;
}
}
public InnerThread1(String name) {
inner = new Inner(name);
}
}
// Using an anonymous inner class:
class InnerThread2 {
private int countDown = 5;
private Thread t;
public InnerThread2(String name) {
t = new Thread(name) {
public void run() {
try {
while(true) {
System.out.println(this);
if(--countDown == 0) return;
sleep(10);
}
} catch(InterruptedException e) {
System.out.println("sleep() interrupted");
}
}
public String toString() {
return getName() + ": " + countDown;
}
};
t.start();
}
}
// Using a named Runnable implementation:
class InnerRunnable1 {
private int countDown = 5;
private Inner inner;
private class Inner implements Runnable {
Thread t;
Inner(String name) {
t = new Thread(this, name);
t.start();
}
public void run() {
try {
while(true) {
System.out.println(this);
if(--countDown == 0) return;
TimeUnit.MILLISECONDS.sleep(10);
}
} catch(InterruptedException e) {
System.out.println("sleep() interrupted");
}
}
public String toString() {
return t.getName() + ": " + countDown;
}
}
public InnerRunnable1(String name) {
inner = new Inner(name);
}
}
// Using an anonymous Runnable implementation:
class InnerRunnable2 {
private int countDown = 5;
private Thread t;
public InnerRunnable2(String name) {
t = new Thread(new Runnable() {
public void run() {
try {
while(true) {
System.out.println(this);
if(--countDown == 0) return;
TimeUnit.MILLISECONDS.sleep(10);
}
} catch(InterruptedException e) {
System.out.println("sleep() interrupted");
}
}
public String toString() {
return Thread.currentThread().getName() +
": " + countDown;
}
}, name);
t.start();
}
}
// A separate method to run some code as a task:
class ThreadMethod {
private int countDown = 5;
private Thread t;
private String name;
public ThreadMethod(String name) { this.name = name; }
public void runTask() {
if(t == null) {
t = new Thread(name) {
public void run() {
try {
while(true) {
System.out.println(this);
if(--countDown == 0) return;
sleep(10);
}
} catch(InterruptedException e) {
System.out.println("sleep() interrupted");
}
}
public String toString() {
return getName() + ": " + countDown;
}
};
t.start();
}
}
}
public class ThreadVariations {
public static void main(String[] args) {
new InnerThread1("InnerThread1");
new InnerThread2("InnerThread2");
new InnerRunnable1("InnerRunnable1");
new InnerRunnable2("InnerRunnable2");
new ThreadMethod("ThreadMethod").runTask();
}
}
十、術語
在Java中,可以選擇如何實現併發編程,並且這個選擇會令人困惑。這個問題通常來自於用來描述併發程序技術的術語,特別是涉及線程的那些。
到目前爲止,應該看到要執行的任務與驅動它的線程之間有一個差異,這個差異在java類庫中尤爲明顯。因爲對Thread類實際上沒有任何的控制權(並且這種隔離在使用執行器時更加明顯,因爲執行器將替你處理線程的創建和管理)。你創建任務,並通過某種方式將一個線程附着到任務上,以使得這個線程可以驅動任務。Thread類自身不執行任何操作,它只是驅動賦予它的任務。
從概念上講,我們希望創建獨立於其他任務運行的任務,因此我們應該能夠定義任務,然後說“開始”,並且不用操心其細節。但是在物理上,創建線程可能代價高昂,因此必須保存並管理它們。這樣從實現角度看,將任務從線程分離出來是很有意義的。
十一、加入一個線程
一個線程可以在其他線程之上調用join()方法,其效果是等待一段時間直到第二個線程結束才能繼續執行。如果某個線程在另一個線程t上調用t.join(),此線程將被掛起,直到目標線程結束才恢復(即t.isAlive()返回爲假)。
也可在調用join()時帶上一個超時參數,這樣如果目標線程在這段時間到期時還沒有結束的話,join()方法總能返回。
對join()方法的調用可以被中斷,做法是在調用線程上調用interrupt()方法,這時需要用到try-catch子句。如下面示例:
class Sleeper extends Thread {
private int duration;
public Sleeper(String name, int sleepTime) {
super(name);
duration = sleepTime;
start();
}
public void run() {
try {
sleep(duration);
} catch(InterruptedException e) {
System.out.println(getName() + " was interrupted. " +
"isInterrupted(): " + isInterrupted());
return;
}
System.out.println(getName() + " has awakened");
}
}
class Joiner extends Thread {
private Sleeper sleeper;
public Joiner(String name, Sleeper sleeper) {
super(name);
this.sleeper = sleeper;
start();
}
public void run() {
try {
sleeper.join();
} catch(InterruptedException e) {
System.out.println("Interrupted");
}
System.out.println(getName() + " join completed");
}
}
public class Joining {
public static void main(String[] args) {
Sleeper
sleepy = new Sleeper("Sleepy", 1500),
grumpy = new Sleeper("Grumpy", 1500);
Joiner
dopey = new Joiner("Dopey", sleepy),
doc = new Joiner("Doc", grumpy);
grumpy.interrupt();
}
} /* Output:
Grumpy was interrupted. isInterrupted(): false
Doc join completed
Sleepy has awakened
Dopey join completed
*/
Sleeper是一個Thread類型,它要休眠一段時間,這段時間是通過構造器傳進來的參數所指定的。在run()中,sleep()方法有可能在指定的時間期滿時返回,但也可能被中斷。在catch子句中,將根據isInterrupted()的返回值來報告這個中斷。當另一個線程在該線程上調用interrupt()時,將給該線程設定一個標誌,標明該線程已被中斷。然而異常捕獲時將清理這個標誌,所以在catch子句中,在異常被捕獲的時候這個標誌總是假的。除異常之外,這個標誌還可以用於其他情況,比如線程可能會檢查其中斷狀態。
Joiner線程通過在Sleeper對象上調用join()方法來等待Sleeper醒來。在main()裏面,每個Sleeper都有一個Joiner,這可以在輸出中發現,如果Sleeper中斷或者是正常結束,Joiner將和Sleeper一同結束。
注意,Java SE5的java.util.concurrent類庫包含諸如CyclicBarrier這樣的工具,它們可能比最初的線程類庫中的join()更加適合。
十二、創建有相應的用戶界面
使用線程的動機之一就是建立有響應的用戶界面。下面給出了一個基於控制檯用戶界面的簡單示例。下面例子有兩個版本:一個關注於運算,所以不能讀取控制檯輸入,另一個把運算放在任務裏單獨運行,此時就可以在進行運算的同時監聽控制檯輸入。
class UnresponsiveUI {
private volatile double d = 1;
public UnresponsiveUI() throws Exception {
while(d > 0)
d = d + (Math.PI + Math.E) / d;
System.in.read(); // Never gets here
}
}
public class ResponsiveUI extends Thread {
private static volatile double d = 1;
public ResponsiveUI() {
setDaemon(true);
start();
}
public void run() {
while(true) {
d = d + (Math.PI + Math.E) / d;
}
}
public static void main(String[] args) throws Exception {
//! new UnresponsiveUI(); // Must kill this process
new ResponsiveUI();
System.in.read();
System.out.println(d); // Shows progress
}
}
UnresponsiveUI 在一個無限循環裏執行運算,顯然程序不可能到達讀取控制檯輸入的那一行。要想讓程序有響應,就得把計算程序放在run()方法中,這樣它就能讓出處理器給別的程序。
十三、線程組
線程組持有一個線程的集合。
十四、捕獲異常
由於線程的本質特性,使得不能捕獲從線程中逃逸的異常。一旦異常逃出任務的run()方法,它就會向外傳播到控制檯,除非採取特殊的步驟捕獲這種錯誤的異常。在java se5之前可以使用線程組來捕獲這些異常,但是之後就可以用Executor來解決這個問題。
下面的任務總是會拋出一個異常,該異常會傳播到其run()方法的外部。
import java.util.concurrent.*;
public class ExceptionThread implements Runnable {
public void run() {
throw new RuntimeException();
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new ExceptionThread());
}
}
將main()的主體放到try-catch語句中是沒有作用的:
import java.util.concurrent.*;
public class NaiveExceptionHandling {
public static void main(String[] args) {
try {
ExecutorService exec =
Executors.newCachedThreadPool();
exec.execute(new ExceptionThread());
} catch(RuntimeException ue) {
// This statement will NOT execute!
System.out.println("Exception has been handled!");
}
}
}
這將產生與前面示例相同的結果:未捕獲的異常。
爲解決這個問題,可以修改Executor產生線程的方式。Thread.UncaughtExceptionHandler是Java SE5中的新接口,它允許在每個Thread對象上附着一個異常處理器。Thread.UncaughtExceptionHandler.uncaughtException()會在線程因未捕獲的異常而臨近死亡時被調用。爲了使用它,創建一個新型的ThreadFactory,它將在每個新創建的Thread對象上附着一個Thread.UncaughtExceptionHandler。將這個工廠傳遞給Executors創建新的ExecService的方法:
import java.util.concurrent.*;
class ExceptionThread2 implements Runnable {
public void run() {
Thread t = Thread.currentThread();
System.out.println("run() by " + t);
System.out.println(
"eh = " + t.getUncaughtExceptionHandler());
throw new RuntimeException();
}
}
class MyUncaughtExceptionHandler implements
Thread.UncaughtExceptionHandler {
public void uncaughtException(Thread t, Throwable e) {
System.out.println("caught " + e);
}
}
class HandlerThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {
System.out.println(this + " creating new Thread");
Thread t = new Thread(r);
System.out.println("created " + t);
t.setUncaughtExceptionHandler(
new MyUncaughtExceptionHandler());
System.out.println(
"eh = " + t.getUncaughtExceptionHandler());
return t;
}
}
public class CaptureUncaughtException {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool(
new HandlerThreadFactory());
exec.execute(new ExceptionThread2());
}
} /* Output: (90% match)
HandlerThreadFactory@de6ced creating new Thread
created Thread[Thread-0,5,main]
eh = MyUncaughtExceptionHandler@1fb8ee3
run() by Thread[Thread-0,5,main]
eh = MyUncaughtExceptionHandler@1fb8ee3
caught java.lang.RuntimeException
*/
上面的示例使得可以按照具體的情況逐個地設置處理器。如果知道將要在代碼中處處使用相同的異常處理器,那麼更簡單的方式是在Thread類中設置一個靜態域,並將這個處理器設置爲默認的未捕獲異常處理器:
import java.util.concurrent.*;
public class SettingDefaultHandler {
public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler(
new MyUncaughtExceptionHandler());
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new ExceptionThread());
}
} /* Output:
caught java.lang.RuntimeException
*/
這個處理器只有在不存在線程專有的未捕獲異常處理器的情況下才會被調用。