在實現多線程時,Java語言提供了三種實現方式:
l 繼承Thread類
l 實現Runnable介面
l 使用Timer和TimerTask組合
一、繼承Thread類
1. 如果一個類繼承了Thread類,則該類就具備了多線程的能力,則該類則可以以多線程的方式進行執行。範例程式碼如下:
public class FirstThread extends Thread{ public static void main(String[] args) { //初始化線程 FirstThread ft = new FirstThread(); //啟動線程 ft.start(); try{ for(int i=0;i<10;++i){ //延時1秒 Thread.sleep(1000); System.out.println("main:"+i); } }catch(Exception e){} } public void run(){ try{ for(int i=0;i<10;++i){ //延時1秒 Thread.sleep(1000); System.out.println("run"+i); } }catch(Exception e){} } } |
2. 線程的代碼必須書寫在run方法內部或者在run方法內部進行調用。
3. 可以把線程以單獨類的形式出現。一個類具備了多線程的能力以後,可以在程式中需要的位置進行啟動,而不僅僅是在main方法內部啟動。
4. 當自訂線程中的run方法執行完成以後,則自訂線程將自然死亡。而對於系統線程來說,只有當main方法執行結束,而且啟動的其它線程都結束以後,才會結束。當系統線程執行結束以後,則程式的執行才真正結束。
5. 在Thread子類中不應該隨意覆蓋start()方法,假如一定要覆蓋start()方法,那麼應該先調用super.start()方法。
6. 當自訂線程中的run方法執行完成以後,則自訂線程將自然死亡。所以一個線程只能被啟動一次,否則會拋出java.lang.IllegalThreadStateException異常。
二、實現Runnable介面
一個類如果需要具備多線程的能力,也可以通過實現java.lang.Runnable介面進行實現。範例程式碼如下:
//MyRunnable.java public class MyRunnable implements Runnable{ public void run(){ try{ for(int i = 0;i < 10;i++){ Thread.sleep(1000); System.out.println("run:" + i); } }catch(Exception e){} } } //Test.java public class Test { public static void main(String[] args) { MyRunnable mr = new MyRunnable(); Thread t = new Thread(mr); t.start(); try{ for(int i = 0;i < 10;i++){ Thread.sleep(1000); System.out.println("main:" + i); } }catch(Exception e){} } } |
三、使用Timer和TimerTask組合
1. 在這種實現方式中,Timer類實現的是類似鬧鐘的功能,也就是定時或者每隔一定時間觸發一次線程。Timer類本身實現的就是一個線程,只是這個線程是用來實現調用其它線程的。而TimerTask類是一個抽象類別,該類實現了Runnable介面,該類具備多線程的能力。
2. 在這種實現方式中,通過繼承TimerTask使該類獲得多線程的能力,將需要多線程執行的代碼書寫在run方法內部,然後通過Timer類啟動線程的執行。
3. 在實際使用時,一個Timer可以啟動任意多個TimerTask實現的線程,但是多個線程之間會存在阻塞。所以如果多個線程之間如果需要完全獨立啟動並執行話,最好還是一個Timer啟動一個TimerTask實現。
4. 以下是範例程式碼:
//MyTimerTask.java import java.util.TimerTask; public class MyTimerTask extends TimerTask{ String s; public MyTimerTask(String s){ this.s = s; } public void run(){ try{ for(int i = 0;i < 10;i++){ Thread.sleep(1000); System.out.println(s + i); } }catch(Exception e){} } } //Test.java import java.util.Timer; public class Test { public static void main(String[] args) { //建立Timer Timer t = new Timer(); //建立TimerTask MyTimerTask mtt1 = new MyTimerTask("線程1:"); //啟動線程 t.schedule(mtt1, 0); } } |
5. Timer類中啟動線程還包含兩個scheduleAtFixedRate方法,這其作用是實現重複啟動線程時的精確延時。
四、線程的狀態轉換
1. Java中的線程有五種基本狀態:建立狀態(New),就緒狀態(Runnable),運行狀態(Running),阻塞狀態(Blocked)和死亡狀態(Dead),這五種狀態的轉換關係如所示:
2. 阻塞狀態是指線程因某些原因放棄CPU,暫時停止運行。分為以下三種:
a) 位於對象等待池中的阻塞狀態(Blocked in object’s wait pool):當線程處於運行狀態時,如果執行了某個對象的wait()方法,Java虛擬機器就會把線程放到這個對象的等待池中。
b) 位於對象鎖池中的阻塞狀態(Blocked in object’s lock pool):當線程處於運行狀態,試圖獲得某個對象的同步鎖時,如果該對象的同步鎖已經被其他線程佔用,Java虛擬機器會把這個線程放到這個對象的鎖池中。
c) 其他阻塞狀態(Otherwise Blocked):當前線程執行了sleep()方法,或者調用了其他線程的join()方法,或者發出了I/O請求時,就會進入這個狀態。
五、線程調度
1. Java虛擬機器採用搶佔式調度模型,線程的調度不是分時的,同時啟動多個線程後,不能保證各個線程輪流獲得均等的CPU時間片。
2. 如果希望明確地讓一個線程給另外一個線程啟動並執行機會,可以採取以下方法之一:
a) 調整各個線程的優先順序。
b) 讓處於運行狀態的線程調用Thread.sleep()方法。
c) 讓處於運行狀態的線程調用Thread.yield()方法。
d) 讓處於運行狀態的線程調用另一個線程的join()方法。
3. Thread類的setPriority(int)和getPriority()方法分別用來設定優先權和讀取優先順序。優先順序用整數表示,取值範圍是1~10,Thread類有以下3個靜態常量:
a) MAX_PRIORITY:取值為10,表示最高優先順序。
b) MIN_PRIORITY:取值為1,表示最低優先順序。
c) NORM_PRIORITY:取值為5,表示預設的優先順序。
4. 主線程的預設優先順序為Thread.NORM_PRIORITY。如果線程A建立了線程B,那麼線程B和線程A具有相同的優先順序。
5. 線程睡眠:Thread.sleep()——當一個線程在運行中執行了sleep()方法時,它就放棄CPU,轉到阻塞狀態。當線程結束睡眠後,首先轉到就緒狀態。如果線程在睡眠時被中斷,就會收到一個InterruptException異常。
6. 線程讓步:Thread.yield()——當線程在運行中執行了Thread類的yield()靜態方法,如果此時具有相同優先順序的其他線程處於就緒狀態,那麼yield()方法將把當前啟動並執行線程放到可運行池中並使另一個線程運行,如果沒有相同優先順序的可運行線程,則yield()方法什麼也不做。
7. sleep()方法和yield()方法都是Thread類的靜態方法,都會使當前處於運行狀態的線程放棄CPU,把運行機會讓給別的線程,兩者的區別在於:
a) sleep()方法會給其他線程運行機會,而不考慮其他線程的優先順序,因此會給較低優先順序線程一個運行機會;yield()方法只會給相同優先順序或者更高優先順序的線程一個啟動並執行機會。
b) 當線程執行了sleep(long millis)方法後,將轉到阻塞狀態,參數millis指定睡眠時間;當線程執行了yield()方法後,將轉到就緒狀態。
c) sleep()方法聲明拋出InterruptException異常,而yield()方法沒有聲明拋出任何異常。
d) sleep()方法比yield()方法具有更好的可移植性,不能依靠yield()方法來提高程式的並發效能。
8. 等待其他線程結束:join()——當前線程可以調用另一個線程的join()方法,當時啟動並執行線程將轉到阻塞狀態,直到另一個線程運行結束,它才會恢複運行。
六、獲得當前線程對象的引用及其他
1. Thread類的currentThread()靜態方法返回當前線程對象的引用。
2. Thread類的getName()執行個體方法返回線程的名字。
3. Thread類的setName()執行個體方法可以顯示地設定線程的名字。
七、後台線程
1. 後台線程是指為其他線程提供服務的線程,也稱為守護線程。
2. 後台線程與前台線程相伴相隨,只有所有的前台線程都結束生命週期,後台線程才會結束生命週期。只要有一個前台線程還沒有運行結束,後台線程就不會結束生命週期。
3. 主線程在預設情況下是前台線程,由前台線程建立的線程在預設情況下也是前台線程。
4. 調用Thread類的setDaemon(true)方法,就能把一個線程設定為後台線程。Thread類的isDaemon()方法用來判斷一個線程是否是後台線程。
5. 使用後台線程,要注意以下幾點:
a) Java虛擬機器所能保證的是,當所有後台線程都運行結束時,假如後台線程還在運行,Java虛擬機器就會終止。此外,後台線程是否一定在前台線程的後面結束生命週期,還取決於程式的實現。
b) 只有線上程啟動前(即調用start()方法前),才能把線程設定為後台線程。如果線程啟動後再調用這個線程的setDaemon()方法,就會導致IllegalThreadStateException異常。
c) 由前台線程建立的線程在預設情況下仍然是前台線程,由後台線程建立的線程在預設情況下仍然是後台線程。
八、線程的同步
1. 原子操作由相關的一組操作完成,這些操作可能會操縱與其他線程共用的資源。一個線程在執行原子操作的期間,必須採取措施使得其他線程不能操縱共用資源。
2. 為了保證每個線程都能正常地執行原子操作,Java引入了同步機制,具體做法是在代表原子操作的程式碼前加上synchronized標記,這樣的代碼被稱為同步代碼塊。
3. 每個Java對象都有且只有一個同步鎖,在任何時刻,最多隻允許一個線程擁有這把鎖。
a) 假如這個鎖已經被其他線程佔用,Java虛擬機器就會把這個線程放到對象的鎖池中,這個線程進入阻塞狀態。在對象的鎖池中可能會有許多等待鎖的線程,等到其他線程釋放了鎖,Java虛擬機器會從鎖池中隨機取出一個線程,使這個線程擁有鎖,並且轉到就緒狀態。
b) 假如這個鎖沒有被其他線程佔用,線程就會獲得這把鎖,開始執行同步代碼塊。在一般情況下,線程只有在執行完同步代碼塊後才會釋放鎖。
4. 如果一個方法中的所有代碼都屬於同步代碼,則可以直接在方法前用synchronized修飾。
5. 當一個線程執行一個對象的同步代碼塊時,其他線程仍然可以執行對象的非同步代碼塊。
6. 在靜態方法前也可以使用synchronized修飾符。
7. 當一個線程開始執行同步代碼塊時,並不意味著必須以不中斷的方式運行,進入同步代碼塊的線程也可以執行Thread.sleep()或者執行Thread.yield()方法,此時它並沒有釋放鎖,只是把運行機會(即CPU)讓給了其他線程。
8. synchronized聲明不會被繼承。
9. 在以下情況下,持有鎖的線程會釋放鎖:
a) 執行完同步代碼塊,就會釋放鎖。
b) 在執行同步代碼塊的過程中,遇到異常而導致線程終止,鎖也會被釋放。
c) 在執行同步代碼塊的過程中,執行了鎖所屬對象的wait()方法,這個線程也會自覺釋放鎖,進入對象的等待池。
10. 除了以上情況外,只要持有鎖的線程還沒有執行完同步代碼塊,就不會釋放鎖。因此在以下情況下,線程不會釋放鎖:
a) 在執行同步代碼塊的過程中,執行了Thread.sleep()方法,當前線程放棄CPU,開始睡眠,在睡眠中不會釋放鎖。
b) 在執行同步代碼塊的過程中,執行了Thread.yield()方法,當前線程釋放CPU,但不會釋放鎖。
c) 在執行同步代碼塊的過程中,其他線程執行了當前線程對象的suspend()方法,當前線程被暫停,但不會釋放鎖。Thread類的suspend()方法已經被廢棄。
11. 避免死結的一個通用的經驗法則是:當幾個線程都要訪問共用資源A、B和C時,保證使每個線程都按照同樣的順序去訪問它們,比如都先訪問A,再訪問B和C。
九、線程通訊
1. java.lang.Object類中提供了兩個用於線程通訊的方法:
a) wait():執行該方法的線程會釋放對象的鎖,Java虛擬機器把該線程放到該對象的等待池中。該線程等待其他線程將它喚醒。
b) notify():執行該方法的線程喚醒在對象的等待池中等待的一個線程,Java虛擬機器從對象的等待池中隨機播放一個線程,把它轉到對象的鎖池中。如果對象的等待池中沒有任何線程,那麼notify()方法什麼也不做。
2. Object類還有一個notifyAll()方法,該方法會把對象的等待池中的所有線程都轉到對象的鎖池中。