Java學習總結之第十三章 多線程


在實現多線程時,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),這五種狀態的轉換關系如下圖所示:

clip_image001

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()方法,該方法會把對象的等待池中的所有線程都轉到對象的鎖池中。


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com