多線程編程——實戰篇(一)
時間:2006-09-12
作者:
axman
在進入實戰篇以前,我們簡單說一下多線程編程的一般原則。
[安全性]是多線程編程的首要原則,如果兩個以上的線程訪問同一對象時,一個線程會損壞另一個線程的數據,這就是違反了安全性原則,這樣的程序是不能進入實際應用的。
安全性的保證可以通過設計安全的類和程序員的手工控制。如果多個線程對同一對象訪問不會危及安全性,這樣的類就是線程安全的類,在JAVA中比如String類就被設計為線程安全的類。而如果不是線程安全的類,那么就需要程序員在訪問這些類的實例時手工控制它的安全性。
[可行性]是多線程編程的另一個重要原則,如果僅僅實現了安全性,程序卻在某一點后不能繼續執行或者多個線程發生死鎖,那么這樣的程序也不能作為真正的多線程程序來應用。
相對而言安全性和可行性是相互抵觸的,安全性越高的程序,可性行會越低。要綜合平衡。
[高性能] 多線程的目的本來就是為了增加程序運行的性能,如果一個多線程完成的工作還不如單線程完成得快。那就不要應用多線程了。
高性能程序主要有以下幾個方面的因素:
數據吞吐率,在一定的時間內所能完成的處理能力。
響應速度,從發出請求到收到響應的時間。
容量,指同時處理雅致同任務的數量。
安全性和可行性是必要條件,如果達到不這兩個原則那就不能稱為真正的多線程程序。而高性是多線程編程的目的,也可以說是充要條件。否則,為什么采用多線程編程呢?
[生產者與消費者模式]
首先以一個生產者和消費者模式來進入實戰篇的第一節。
生產者和消費者模式中保護的是誰?
多線程編程都在保護著某些對象,這些個對象是"緊俏資源",要被最大限度地利用,這也是采用多線程方式的理由。在生產者消費者模式中,我們要保護的是"倉庫",在我下面的這個例子中,
就是桌子(table)。
我這個例子的模式完全是生產者-消費者模式,但我換了個名字。廚師-食客模式,這個食堂中只有1張桌子,同時最多放10個盤子,現在有4個廚師做菜,每做好一盤就往桌子上放(生產者將產品往倉庫中放),而有6個食客不停地吃(消費者消費產品,為了說明問題,他們的食量是無限的)。
一般而言,廚師200-400ms做出一盤菜,而食客要400-600ms吃完一盤。當桌子上放滿了10個盤子后,所有廚師都不能再往桌子上放,而當桌子是沒有盤子時,所有的食客都只好等待。
下面我們來設計這個程序:
因為我們不知道具體是什么菜,所以叫它food:
class Food{}
然后是桌子,因為它要有序地放而且要有序地取(不能兩個食客同時爭取第三盤菜),所以我們擴展LinkedList,或者你用聚合把一個LinkedList作為屬性也能達到同樣的目的,例子中我是用
繼承,從構造方法中傳入一個可以放置的最大值。
class Table extends java.util.LinkedList{
int maxSize;
public Table(int maxSize){
this.maxSize = maxSize;
}
}
現在我們要為它加兩個方法,一是廚師往上面放菜的方法,一是食客從桌子上拿菜的方法。
放菜:因為一張桌子由多個廚師放菜,所以廚師放菜的要被同步,如果桌子上已經有十盤菜了。所有廚師就要等待:
public synchronized void putFood(Food f){
while(this.size() >= this.maxSize){
try{
this.wait();
}catch(Exception e){}
}
this.add(f);
notifyAll();
}
拿菜:同上面,如果桌子上一盤菜也沒有,所有食客都要等待:
public synchronized Food getFood(){
while(this.size() <= 0){
try{
this.wait();
}catch(Exception e){}
}
Food f = (Food)this.removeFirst();
notifyAll();
return f;
}
廚師類:
由于多個廚師要往一張桌子上放菜,所以他們要操作的桌子應該是同一個對象,我們從構造方法中將桌子對象傳進去以便控制在主線程中只產生一張桌子。
廚師做菜要用一定的時候,我用在make方法中用sleep表示他要消耗和時候,用200加上200的隨機數保證時間有200-400ms中。做好后就要往桌子上放。
這里有一個非常重要的問題一定要注意,就是對什么范圍同步的問題,因為產生競爭的是桌子,所以所有putFood是同步的,而我們不能把廚師自己做菜的時間也放在同步中,因為做菜是各自做的。同樣食客吃菜的時候也不應該同步,只有從桌子中取菜的時候是競爭的,而具體吃的時候是各自在吃。所以廚師類的代碼如下:
class Chef extends Thread{
Table t;
Random r = new Random(12345);
public Chef(Table t){
this.t = t;
}
public void run(){
while(true){
Food f = make();
t.putFood(f);
}
}
private Food make(){
try{
Thread.sleep(200+r.nextInt(200));
}catch(Exception e){}
return new Food();
}
}
同理我們產生食客類的代碼如下:
class Eater extends Thread{
Table t;
Random r = new Random(54321);
public Eater(Table t){
this.t = t;
}
public void run(){
while(true){
Food f = t.getFood();
eat(f);
}
}
private void eat(Food f){
try{
Thread.sleep(400+r.nextInt(200));
}catch(Exception e){}
}
}
完整的程序在這兒:
package debug;
import java.util.regex.*;
import java.util.*;
class Food{}
class Table extends LinkedList{
int maxSize;
public Table(int maxSize){
this.maxSize = maxSize;
}
public synchronized void putFood(Food f){
while(this.size() >= this.maxSize){
try{
this.wait();
}catch(Exception e){}
}
this.add(f);
notifyAll();
}
public synchronized Food getFood(){
while(this.size() <= 0){
try{
this.wait();
}catch(Exception e){}
}
Food f = (Food)this.removeFirst();
notifyAll();
return f;
}
}
class Chef extends Thread{
Table t;
String name;
Random r = new Random(12345);
public Chef(String name,Table t){
this.t = t;
this.name = name;
}
public void run(){
while(true){
Food f = make();
System.out.println(name+" put a Food:"+f);
t.putFood(f);
}
}
private Food make(){
try{
Thread.sleep(200+r.nextInt(200));
}catch(Exception e){}
return new Food();
}
}
class Eater extends Thread{
Table t;
String name;
Random r = new Random(54321);
public Eater(String name,Table t){
this.t = t;
this.name = name;
}
public void run(){
while(true){
Food f = t.getFood();
System.out.println(name+" get a Food:"+f);
eat(f);
}
}
private void eat(Food f){
try{
Thread.sleep(400+r.nextInt(200));
}catch(Exception e){}
}
}
public class Test {
public static void main(String[] args) throws Exception{
Table t = new Table(10);
new Chef("Chef1",t).start();
new Chef("Chef2",t).start();
new Chef("Chef3",t).start();
new Chef("Chef4",t).start();
new Eater("Eater1",t).start();
new Eater("Eater2",t).start();
new Eater("Eater3",t).start();
new Eater("Eater4",t).start();
new Eater("Eater5",t).start();
new Eater("Eater6",t).start();
}
}
這一個例子中,我們主要關注以下幾個方面:
1.同步方法要保護的對象,本例中是保護桌子,不能同時往上放菜或同時取菜。
假如我們把putFood方法和getFood方法在廚師類和食客類中實現,那么我們應該如此:
(以putFood為例)
class Chef extends Thread{
Table t;
String name;
public Chef(String name,Table t){
this.t = t;
this.name = name;
}
public void run(){
while(true){
Food f = make();
System.out.println(name+" put a Food:"+f);
putFood(f);
}
}
private Food make(){
Random r = new Random(200);
try{
Thread.sleep(200+r.nextInt());
}catch(Exception e){}
return new Food();
}
public void putFood(Food f){//方法本身不能同步,因為它同步的是this.即Chef的實例
synchronized (t) {//要保護的是t
while (t.size() >= t.maxSize) {
try {
t.wait();
}
catch (Exception e) {}
}
t.add(f);
t.notifyAll();
}
}
}
2.同步的范圍,在本例中是放和取兩個方法,不能把做菜和吃菜這種各自不相干的工作放在受保護的范圍中。
3.參與者與容積比
對于生產者和消費者的比例,以及桌子所能放置最多菜的數量三者之間的關系是影響性能的重要因素,如果是過多的生產者在等待,則要增加消費者或減少生產者的數據,反之則增加生產者或減少消費者的數量。
另外如果桌子有足夠的容量可以很大程序提升性能,這種情況下可以同時提高生產者和消費者的數量,但足夠大的容時往往你要有足夠大的物理內存。
=========================================================================
多線程編程——實戰篇(二)
時間:2006-11-21
作者:
axman
本節繼續上一節的討論。
[一個線程在進入對象的休息室(調用該對象的wait()方法)后會釋放對該對象的鎖],基于這個原因。在同步中,除非必要,否則你不應用使用Thread.sleep(long l)方法,因為sleep方法并不釋放對象的鎖。
這是一個極其惡劣的品德,你自己什么事也不干,進入sleep狀態,卻抓住競爭對象的監視鎖不讓其它需要該對象監視鎖的線程運行,簡單說是極端自私的一種行為。但我看到過很多程序員仍然有在同步方法中調用sleep的代碼。
看下面的例子:
package debug;
class SleepTest{
public synchronized void wantSleep(){
try{
Thread.sleep(1000*60);
}catch(Exception e){}
System.out.println("111");
}
public synchronized void say(){
System.out.println("123");
}
}
class T1 extends Thread{
SleepTest st;
public T1(SleepTest st){
this.st = st;
}
public void run(){
st.wantSleep();
}
}
class T2 extends Thread{
SleepTest st;
public T2(SleepTest st){
this.st = st;
}
public void run(){
st.say();
}
}
public class Test {
public static void main(String[] args) throws Exception{
SleepTest st = new SleepTest();
new T1(st).start();
new T2(st).start();
}
}
我們看到,線程T1的實例運行后,當前線程抓住了st實例的鎖,然后進入了sleep。直到它睡滿60秒后才運行到System.out.println("111");然后run方法運行完成釋放了對st的監視鎖,線程T2的實例才得到運行的機會。
而如果我們把wantSleep方法改成:
public synchronized void wantSleep(){
try{
//Thread.sleep(1000*60);
this.wait(1000*60);
}catch(Exception e){}
System.out.println("111");
}
我們看到,T2的實例所在的線程立即就得到了運行機會,首先打印了123,而T1的實例所在的線程仍然等待,直到等待60秒后運行到System.out.println("111");方法。
所以,調用wait(long l)方法不僅達到了阻塞當前線程規定時間內不運行,而且讓其它有競爭需求的線程有了運行機會,這種利人不損己的方法,何樂而不為?這也是一個有良心的程序員應該遵循的原則。
當一個線程調用wait(long l)方法后,線程如果繼續運行,你無法知道它是等待時間完成了還是在wait時被其它線程喚醒了,如果你非常在意它一定要等待足夠的時間才執行某任務,而不希望是中途被喚醒,這里有一個不是非常準確的方法:
long l = System.System.currentTimeMillis();
wait(1000);//準備讓當前線程等待1秒
while((System.System.currentTimeMillis() - l) < 1000)//執行到這里說明它還沒有等待到1秒
//是讓其它線程給鬧醒了
wait(1000-(System.System.currentTimeMillis()-l));//繼續等待余下的時間.
這種方法不是很準確,但基本上能達到目的。
所以在同步方法中,除非你明確知道自己在干什么,非要這么做的話,你沒有理由使用sleep,wait方法足夠達到你想要的目的。而如果你是一個很保守的人,看到上面這段話后,你對sleep方法深惡痛絕,堅決不用sleep了,那么在非同步的方法中(沒有和其它線程競爭的對象),你想讓當前線程阻塞一定時間后再運行,應該如何做呢?(這完全是一種賣弄,在非同步的方法中你就應該合理地應用sleep嘛,但如果你堅決不用sleep,那就這樣來做吧)
public static mySleep(long l){
Object o = new Object();
synchronized(o){
try{
o.wait(l);
}catch(Exception e){}
}
}
放心吧,沒有人能在這個方法外調用o.notify[All],所以o.wait(l)會一直等到設定的時間才會運行完成。
[虛擬鎖的使用]
虛擬鎖簡單說就是不要調用synchronized方法(它等同于synchronized(this))和不要調用synchronized(this),這樣所有調用在這個實例上的所有同步方法的線程只能有一個線程可以運行。也就是說:
如果一個類有兩個同步方法 m1,m2,那么不僅是兩個以上線調用m1方法的線程只有一個能運行,就是兩個分別調用m1,m2的線程也只有一個能運行。當然非同步方法不存在任何競爭,在一個線程獲取該對象的監視鎖后這個對象的非同步方法可以被任何線程調用。
而大多數時候,我們可能會出現這種情況,多個線程調用m1時需要保護一種資源,而多個線程調用M2時要保護的是另一種資源,如果我們把m1,m2都設成同步方法。兩個分別調用這兩個方法的線程其實并不產生沖突,但它們都要獲取這個實例的鎖(同步方法是同步this)而產生了不必要競爭。
所以這里應該采用虛擬鎖。
即將m1和m2方法中各自保護的對象作為屬性a1,a2傳進來,然后將同步方法改為方法的同步塊分別以a1,a2為參數,這樣到少是不同線程調用這兩個不同方法時不會產生競爭,當然如果m1,m2方法都操作同一受保護對象則兩個方法還是應該作為同步方法。這也是應該將方法同步還是采用同步塊的理由之一。
package debug;
class SleepTest{
public synchronized void m1(){
System.out.println("111");
try{
Thread.sleep(10000);
}catch(Exception e){}
}
public synchronized void m2(){
System.out.println("123");
}
}
class T1 extends Thread{
SleepTest st;
public T1(SleepTest st){
this.st = st;
}
public void run(){
st.m1();
}
}
class T2 extends Thread{
SleepTest st;
public T2(SleepTest st){
this.st = st;
}
public void run(){
st.m2();
}
}
public class Test {
public static void main(String[] args) throws Exception{
SleepTest st = new SleepTest();
new T1(st).start();
new T2(st).start();
}
}
這個例子可以看到兩個線程分別調用st實例的m1和m2方法卻因為都要獲取st的監視鎖而產生了競爭。T2實例要在T1運行完成后才能運行(間隔了10秒)。而假設m1方法要操作操作一個文件 f1,m2方法要操作一個文件f2,當然我們可以在方法中分別同步f1,f2,但現在還不知道f2,f2是否存在,如果不存在我們就同步了一個null對象,那么我們可以使用虛擬鎖:
package debug;
class SleepTest{
String vLock1 = "vLock1";
String vLock2 = "vLock2";
public void m1(){
synchronized(vLock1){
System.out.println("111");
try {
Thread.sleep(10000);
}
catch (Exception e) {}
//操作f1
}
}
public void m2(){
synchronized(vLock2){
System.out.println("123");
//操作f2
}
}
}
class T1 extends Thread{
SleepTest st;
public T1(SleepTest st){
this.st = st;
}
public void run(){
st.m1();
}
}
class T2 extends Thread{
SleepTest st;
public T2(SleepTest st){
this.st = st;
}
public void run(){
st.m2();
}
}
public class Test {
public static void main(String[] args) throws Exception{
SleepTest st = new SleepTest();
new T1(st).start();
new T2(st).start();
}
}
我們看到兩個分別調用m1和m2的線程由于它們獲取不同對象的監視鎖,它們沒有任何競爭就正常運行,只有這兩個線程同時調用m1或m2才會產生阻塞。
=========================================================================
多線程編程——實戰篇(三)
時間:2006-12-28
作者:
axman
[深入了解線程對象與線程,線程與運行環境]
在基礎篇中的第一節,我就強調過,要了解多線程編程,首要的兩個概念就是線程對象和線程。現在我們來深入理解線程對象,線程,運行環境之間的關系,弄清Runnable與Thread的作用。
在JAVA平臺中,序列化機制是一個非常重要的機制,如果不能理解并熟練應用序列化機制,你就不能稱得上一個java程序員。
在JAVA平臺中,為什么有些對象中可序列化的,而有些對象就不能序列化?
能序列化的對象,簡單說是一種可以復制(意味著可以按一定機制進行重構它)的對象,這種對象說到底就是內存中一些數據的組合。只要按一定位置和順序組合就能完整反映這個對象。
而有些對象,是和當前環境相關的,它反映了當前運行的環境和時序,所以不能被序列,否則在另外的環境和時序中就無法“還原”。
比如,一個Socket對象:
Socket sc = new Socket("111.111.111.111",80);
這個sc對象表示當前正在運行這段代碼的主機和IP為"111.111.111.111"的80端口之間建立的一個物理連結,如果它被序列化,那么在另一個時刻在另一個主機上它如何能被還原?Socket連結一旦斷開,就已經不存在,它不可能在另一個時間被另一個主機所重現。重現的已經不是原來那個sc對象了。
線程對象也是這種不可序列化對象,當我們new Thread時,已經初始化了當前這個線程對象所在有主機的運行環境相關的信息,線程調度機制,安全機制等只特定于當前運行環境的信息,假如它被序列化,在另一個環境中運行的時候原來初始化的運行環境的信息就不可能在新的環境中運行。而假如要重新初始化,那它已經不是原來那個線程對象了。
正如Socket封裝了兩個主機之間的連結,但它們并不是已經連結關傳送數據了。要想傳送數據,你還要getInputStream和getOutputStream,并read和write,兩臺主機之間才開始真正的“數據連結”。
一個Thread對象并建立后,只是有了可以"運行"的令牌,僅僅只是一個"線程對象"。只有當它調用start()后,當前環境才會分配給它一個運行的"空間",讓這段代碼開始運行。這個運行的"空間",才叫真正的"線程"。也就是說,真正的線程是指當前正在執行的那一個"事件"。是那個線程對象所在的運行環境。
明白了上面的概念,我們再來看看JAVA中為什么要有Runnable對象和Thread對象。
一、從設計技巧上說,JAVA中為了實現回調,無法調用方法指針,那么利用接口來約束實現者強制提供匹配的方法,并將實現該接口的類的實例作為參數來提供給調用者,這是JAVA平臺實現回調的重要手段。
二、但是從實際的操作來看,對于算法和數據,是不依賴于任何環境的。所以把想要實現的操作中的算法和數據封裝到一個run方法中(由于算法本身是數據的一個部分,所以我把它們合并稱為數據),可以將離數據和環境的邏輯分離開來。使程序員只關心如何實現我想做的操作,而不要關心它所在的環境。當真正的需要運行的時候再將這段"操作"傳給一個具體當前環境的Thread對象。
三、這是最最重要的原因:實現數據共享
因為一個線程對象不對多次運行。所以把數據放在Thread對象中,不會被多個線程同時訪問。簡單說:
class T extends Thread{
Object x;
public void run(){//......;}
}
T t = new T();
當T的實例t運行后,t所包含的數據x只能被一個t.start();對象共享,除非聲明成 static Object x;
一個t的實例數據只能被一個線程訪問。意思是"一個數據實例對應一個線程"。
而假如我們從外部傳入數據,比如
class T extends Thread{
private Object x;
public T(Object x){
this.x = x;
}
public void run(){//......;}
}
這樣我們就可以先生成一個x對象傳給多個Thread對象,多個線程共同操作一個數據。也就是"一個數據實例對應多個線程"。
現在我們把數據更好地組織一下,把要操作的數據Object x和要進行的操作一個封裝到Runnable的run()方法中,把Runnable實例從外部傳給多個Thread對象。這樣,我們就有了:
[一個對象的多個線程]
這是以后我們要介紹的線程池的重要概念。
========================================================================
多線程編程——實戰篇(四)
時間:2007-02-08
作者:
axman
不客氣地說,至少有一半人認為,線程的“中斷”就是讓線程停止。如果你也這么認為,那你對多線程編程還沒有入門。
在java中,線程的中斷(interrupt)只是改變了線程的中斷狀態,至于這個中斷狀態改變后帶來的結果,那是無法確定的,有時它更是讓停止中的線程繼續執行的唯一手段。不但不是讓線程停止運行,反而是繼續執行線程的手段。
對于執行一般邏輯的線程,如果調用它的interrupt()方法,那么對這個線程沒有任何影響,比如線程a正在執行:while(條件) x ++;這樣的語句,如果其它線程調用a.interrupt();那么并不會影響a對象上運行的線程,如果在其它線程里測試a的中斷狀態它已經改變,但并不會停止這個線程的運行。在一個線程對象上調用interrupt()方法,真正有影響的是wait,join,sleep方法,當然這三個方法包括它們的重載方法。
請注意:[上面這三個方法都會拋出InterruptedException],記住這句話,下面我會重復。一個線程在調用interrupt()后,自己不會拋出InterruptedException異常,所以你看到interrupt()并沒有拋出這個異常,所以我上面說如果線程a正在執行while(條件) x ++;你調用a.interrupt();后線程會繼續正常地執行下去。
但是,如果一個線程被調用了interrupt()后,它的狀態是已中斷的。這個狀態對于正在執行wait,join,sleep的線程,卻改變了線程的運行結果。
一、對于wait中等待notify/notifyAll喚醒的線程,其實這個線程已經“暫停”執行,因為它正在某一對象的休息室中,這時如果它的中斷狀態被改變,那么它就會拋出異常。這個InterruptedException異常不是線程拋出的,而是wait方法,也就是對象的wait方法內部會不斷檢查在此對象上休息的線程的狀態,如果發現哪個線程的狀態被置為已中斷,則會拋出InterruptedException,意思就是這個線程不能再等待了,其意義就等同于喚醒它了。
這里唯一的區別是,被notify/All喚醒的線程會繼續執行wait下面的語句,而在wait中被中斷的線程則將控制權交給了catch語句。一些正常的邏輯要被放到catch中來運行。但有時這是唯一手段,比如一個線程a在某一對象b的wait中等待喚醒,其它線程必須獲取到對象b的監視鎖才能調用b.notify()[All],否則你就無法喚醒線程a,但在任何線程中可以無條件地調用a.interrupt();來達到這個目的。只是喚醒后的邏輯你要放在catch中,當然同notify/All一樣,繼續執行a線程的條件還是要等拿到b對象的監視鎖。
二、對于sleep中的線程,如果你調用了Thread.sleep(一年);現在你后悔了,想讓它早些醒過來,調用interrupt()方法就是唯一手段,只有改變它的中斷狀態,讓它從sleep中將控制權轉到處理異常的catch語句中,然后再由catch中的處理轉換到正常的邏輯。同樣地,于join中的線程你也可以這樣處理。
對于一般介紹多線程模式的書上,他們會這樣來介紹:當一個線程被中斷后,在進入wait,sleep,join方法時會拋出異常。是的,這一點也沒有錯,但是這有什么意義呢?如果你知道那個線程的狀態已經處于中斷狀態,為什么還要讓它進入這三個方法呢?當然有時是必須這么做的,但大多數時候沒有這么做的理由,所以我上面主要介紹了在已經調用這三個方法的線程上調用interrupt()方法讓它從"暫停"狀態中恢復過來。這個恢復過來就可以包含兩個目的:
一、[可以使線程繼續執行],那就是在catch語句中招待醒來后的邏輯,或由catch語句轉回正常的邏輯。總之它是從wait,sleep,join的暫停狀態活過來了。
二、[可以直接停止線程的運行],當然在catch中什么也不處理,或return,那么就完成了當前線程的使命,可以使在上面“暫停”的狀態中立即真正的“停止”。
中斷線程
有了上一節[線程的中斷],我們就好進行如何[中斷線程]了。這絕對不是玩一個文字游戲。是因為“線程的中斷”并不能保證“中斷線程”,所以我要特別地分為兩節來說明。這里說的“中斷線程”意思是“停止線程”,而為什么不用“停止線程”這個說法呢?因為線程有一個明確的stop方法,但它是反對使用的,所以請大家記住,在java中以后不要提停止線程這個說法,忘記它!但是,作為介紹線程知識的我,我仍然要告訴你為什么不用“停止線程”的理由。
[停止線程]
當在一個線程對象上調用stop()方法時,這個線程對象所運行的線程就會立即停止,并拋出特殊的ThreadDeath()異常。這里的“立即”因為太“立即”了,就象一個正在擺弄自己的玩具的孩子,聽到大人說快去睡覺去,就放著滿地的玩具立即睡覺去了。這樣的孩子是不乖的。
假如一個線程正在執行:
synchronized void {
x = 3;
y = 4;
}
由于方法是同步的,多個線程訪問時總能保證x,y被同時賦值,而如果一個線程正在執行到x = 3;時,被調用了 stop()方法,即使在同步塊中,它也干脆地stop了,這樣就產生了不完整的殘廢數據。而多線程編程中最最基礎的條件要保證數據的完整性,所以請忘記線程的stop方法,以后我們再也不要說“停止線程”了。
如何才能“結束”一個線程?
[中斷線程]
結束一個線程,我們要分析線程的運行情況。也就是線程正在干什么。如果那個孩子什么事也沒干,那就讓他立即去睡覺。而如果那個孩子正在擺弄他的玩具,我們就要讓它把玩具收拾好再睡覺。
所以一個線程從運行到真正的結束,應該有三個階段:
- 正常運行.
- 處理結束前的工作,也就是準備結束.
- 結束退出.
在我的JDBC專欄中我N次提醒在一個SQL邏輯結束后,無論如何要保證關閉Connnection那就是在finally從句中進行。同樣,線程在結束前的工作應該在finally中來保證線程退出前一定執行:
try{
正在邏輯
}catch(){}
finally{
清理工作
}
那么如何讓一個線程結束呢?既然不能調用stop,可用的只的interrupt()方法。但interrupt()方法只是改變了線程的運行狀態,如何讓它退出運行?對于一般邏輯,只要線程狀態已經中斷,我們就可以讓它退出,所以這樣的語句可以保證線程在中斷后就能結束運行:
while(!isInterrupted()){
正常邏輯
}
這樣如果這個線程被調用interrupt()方法,isInterrupted()為true,就會退出運行。但是如果線程正在執行wait,sleep,join方法,你調用interrupt()方法,這個邏輯就不完全了。
如果一個有經驗的程序員來處理線程的運行的結束:
public void run(){
try{
while(!isInterrupted()){
正常工作
}
}
catch(Exception e){
return;
}
finally{
清理工作
}
}
我們看到,如果線程執行一般邏輯在調用innterrupt后,isInterrupted()為true,退出循環后執行清理工作后結束,即使線程正在wait,sleep,join,也會拋出異常執行清理工作后退出。
這看起來非常好,線程完全按最我們設定的思路在工作。但是,并不是每個程序員都有這種認識,如果他聰明的自己處理異常會如何?事實上很多或大多數程序員會這樣處理:
public void run(){
while(!isInterrupted()){
try{
正常工作
}catch(Exception e){
//nothing
}
finally{
}
}
}
}
想一想,如果一個正在sleep的線程,在調用interrupt后,會如何?wait方法檢查到isInterrupted()為true,拋出異常,而你又沒有處理。而一個拋出了InterruptedException的線程的狀態馬上就會被置為非中斷狀態,如果catch語句沒有處理異常,則下一次循環中isInterrupted()為false,線程會繼續執行,可能你N次拋出異常,也無法讓線程停止。
那么如何能確保線程真正停止?在線程同步的時候我們有一個叫“二次惰性檢測”(double check),能在提高效率的基礎上又確保線程真正中同步控制中。那么我把線程正確退出的方法稱為“雙重安全退出”,即不以isInterrupted()為循環條件。而以一個標記作為循環條件:
class MyThread extend Thread{
private boolean isInterrupted = false;//這一句以后要修改
public void interrupt(){
isInterrupted = true;
super.interrupt();
}
public void run(){
while(!isInterrupted){
try{
正常工作
}catch(Exception e){
//nothing
}
finally{
}
}
}
}
試試這段程序,可以正確工作嗎?
對于這段程序仍然還有很多可說的地方,先到這里吧。
=======================================================================
http://dev2dev.bea.com.cn/bbsdoc/20070208338913.html
posted on 2007-11-09 13:44
lk 閱讀(350)
評論(0) 編輯 收藏 所屬分類:
j2se