[多線程編程的一般原則]

在進(jìn)入實(shí)戰(zhàn)篇以前,我們簡(jiǎn)單說(shuō)一下多線程編程的一般原則。

  [安全性]是多線程編程的首要原則,如果兩個(gè)以上的線程訪問(wèn)同一對(duì)象時(shí),一個(gè)線程會(huì)損壞另一個(gè)線程的數(shù)據(jù),這就是違反了安全性原則,這樣的程序是不能進(jìn)入實(shí)際應(yīng)用的。

  安全性的保證可以通過(guò)設(shè)計(jì)安全的類和程序員的手工控制。如果多個(gè)線程對(duì)同一對(duì)象訪問(wèn)不會(huì)危及安全性,這樣的類就是線程安全的類,在JAVA中比如String類就被設(shè)計(jì)為線程安全的類。而如果不是線程安全的類,那么就需要程序員在訪問(wèn)這些類的實(shí)例時(shí)手工控制它的安全性。

  [可行性]是多線程編程的另一個(gè)重要原則,如果僅僅實(shí)現(xiàn)了安全性,程序卻在某一點(diǎn)后不能繼續(xù)執(zhí)行或者多個(gè)線程發(fā)生死鎖,那么這樣的程序也不能作為真正的多線程程序來(lái)應(yīng)用。

  相對(duì)而言安全性和可行性是相互抵觸的,安全性越高的程序,可性行會(huì)越低。要綜合平衡。

  [高性能] 多線程的目的本來(lái)就是為了增加程序運(yùn)行的性能,如果一個(gè)多線程完成的工作還不如單線程完成得快。那就不要應(yīng)用多線程了。

  高性能程序主要有以下幾個(gè)方面的因素:

  數(shù)據(jù)吞吐率,在一定的時(shí)間內(nèi)所能完成的處理能力。

  響應(yīng)速度,從發(fā)出請(qǐng)求到收到響應(yīng)的時(shí)間。

  容量,指同時(shí)處理雅致同任務(wù)的數(shù)量。

安全性和可行性是必要條件,如果達(dá)到不這兩個(gè)原則那就不能稱為真正的多線程程序。而高性是多線程編程的目的,也可以說(shuō)是充要條件。否則,為什么采用多線程編程呢?

 

[生產(chǎn)者與消費(fèi)者模式]

  生產(chǎn)者和消費(fèi)者模式中保護(hù)的是誰(shuí)?

  多線程編程都在保護(hù)著某些對(duì)象,這些個(gè)對(duì)象是"緊俏資源",要被最大限度地利用,這也是采用多線程方式的理由。在生產(chǎn)者消費(fèi)者模式中,我們要保護(hù)的是"倉(cāng)庫(kù)",在我下面的這個(gè)例子中,就是桌子(table)。
 我這個(gè)例子的模式完全是生產(chǎn)者-消費(fèi)者模式,但我換了個(gè)名字。廚師-食客模式,這個(gè)食堂中只有1張桌子,同時(shí)最多放10個(gè)盤子,現(xiàn)在有4個(gè)廚師做菜,每做好一盤就往桌子上放(生產(chǎn)者將產(chǎn)品往倉(cāng)庫(kù)中放),而有6個(gè)食客不停地吃(消費(fèi)者消費(fèi)產(chǎn)品,為了說(shuō)明問(wèn)題,他們的食量是無(wú)限的)。
一般而言,廚師200-400ms做出一盤菜,而食客要400-600ms吃完一盤。當(dāng)桌子上放滿了10個(gè)盤子后,所有廚師都不能再往桌子上放,而當(dāng)桌子是沒(méi)有盤子時(shí),所有的食客都只好等待。

  下面我們來(lái)設(shè)計(jì)這個(gè)程序:

  因?yàn)槲覀儾恢谰唧w是什么菜,所以叫它food:

class Food{}  然后是桌子,因?yàn)樗行虻胤哦乙行虻厝?不能兩個(gè)食客同時(shí)爭(zhēng)取第三盤菜),所以我們擴(kuò)展LinkedList,或者你用聚合把一個(gè)LinkedList作為屬性也能達(dá)到同樣的目的,例子中我是用繼承,從構(gòu)造方法中傳入一個(gè)可以放置的最大值。

 

class Table extends java.util.LinkedList{
  
int maxSize;
  
public Table(int maxSize){
    
this.maxSize = maxSize;
  }

}

現(xiàn)在我們要為它加兩個(gè)方法,一是廚師往上面放菜的方法,一是食客從桌子上拿菜的方法。
放菜:因?yàn)橐粡堊雷佑啥鄠€(gè)廚師放菜,所以廚師放菜的要被同步,如果桌子上已經(jīng)有十盤菜了。所有廚師就要等待:

public synchronized void putFood(Food f){
    
while(this.size() >= this.maxSize){
      
try{
        
this.wait();
      }
catch(Exception e){}
    }

    
this.add(f);
    notifyAll();
  }

 拿菜:同上面,如果桌子上一盤菜也沒(méi)有,所有食客都要等待:

public synchronized Food getFood(){
    
while(this.size() <= 0){
      
try{
        
this.wait();
      }
catch(Exception e){}
    }

    Food f 
= (Food)this.removeFirst();
    notifyAll();
    
return f;
  }

 

  廚師類:

  由于多個(gè)廚師要往一張桌子上放菜,所以他們要操作的桌子應(yīng)該是同一個(gè)對(duì)象,我們從構(gòu)造方法中將桌子對(duì)象傳進(jìn)去以便控制在主線程中只產(chǎn)生一張桌子。

廚師做菜要用一定的時(shí)候,我用在make方法中用sleep表示他要消耗和時(shí)候,用200加上200的隨機(jī)數(shù)保證時(shí)間有200-400ms中。做好后就要往桌子上放。

這里有一個(gè)非常重要的問(wèn)題一定要注意,就是對(duì)什么范圍同步的問(wèn)題,因?yàn)楫a(chǎn)生競(jìng)爭(zhēng)的是桌子,所以所有putFood是同步的,而我們不能把廚師自己做菜的時(shí)間也放在同步中,因?yàn)樽霾耸歉髯宰龅摹M瑯邮晨统圆说臅r(shí)候也不應(yīng)該同步,只有從桌子中取菜的時(shí)候是競(jìng)爭(zhēng)的,而具體吃的時(shí)候是各自在吃。所以廚師類的代碼如下:

 

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();
  }

}

同理我們產(chǎn)生食客類的代碼如下:

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){}
  }

}

測(cè)試如下:

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();

    }

}
 

 

這一個(gè)例子中,我們主要關(guān)注以下幾個(gè)方面:

  1.同步方法要保護(hù)的對(duì)象,本例中是保護(hù)桌子,不能同時(shí)往上放菜或同時(shí)取菜。

    假如我們把putFood方法和getFood方法在廚師類和食客類中實(shí)現(xiàn),那么我們應(yīng)該如此:

 

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){//方法本身不能同步,因?yàn)樗降氖莟his.即Chef的實(shí)例

    
synchronized (t) {//要保護(hù)的是t
      while (t.size() >= t.maxSize) {
        
try {
          t.wait();
        }

        
catch (Exception e) {}
      }

      t.add(f);
      t.notifyAll();
    }

  }

}

     2.同步的范圍,在本例中是放和取兩個(gè)方法,不能把做菜和吃菜這種各自不相干的工作放在受保護(hù)的范圍中。

 3參與者與容積比

   對(duì)于生產(chǎn)者和消費(fèi)者的比例,以及桌子所能放置最多菜的數(shù)量三者之間的關(guān)系是影響性能的重要因素,如果是過(guò)多的生產(chǎn)者在等待,則要增加消費(fèi)者或減少生產(chǎn)者的數(shù)據(jù),反之則增加生產(chǎn)者或減少消費(fèi)者的數(shù)量。



[一個(gè)線程在進(jìn)入對(duì)象的休息室(調(diào)用該對(duì)象的wait()方法)后會(huì)釋放對(duì)該對(duì)象的鎖]

基于這個(gè)原因。在同步中,除非必要,否則你不應(yīng)用使用Thread.sleep(long l)方法,因?yàn)閟leep方法并不釋放對(duì)象的鎖。
這是一個(gè)極其惡劣的品德,你自己什么事也不干,進(jìn)入sleep狀態(tài),卻抓住競(jìng)爭(zhēng)對(duì)象的監(jiān)視鎖不讓其它需要該對(duì)象監(jiān)視鎖的線程運(yùn)行,簡(jiǎn)單說(shuō)是極端自私的一種行為。但我看到過(guò)很多程序員仍然有在同步方法中調(diào)用sleep的代碼。看下面的例子:

 

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的實(shí)例運(yùn)行后,當(dāng)前線程抓住了st實(shí)例的鎖,然后進(jìn)入了sleep。直到它睡滿60秒后才運(yùn)行到System.out.println("111");然后run方法運(yùn)行完成釋放了對(duì)st的監(jiān)視鎖,線程T2的實(shí)例才得到運(yùn)行的機(jī)會(huì)。而如果我們把wantSleep方法改成:

public synchronized void wantSleep(){
    
try{
      
//Thread.sleep(1000*60);
      this.wait(1000*60);
    }
catch(Exception e){}
    System.out.println(
"111");
  }
 
我們看到,T2的實(shí)例所在的線程立即就得到了運(yùn)行機(jī)會(huì),首先打印了123,而T1的實(shí)例所在的線程仍然等待,直到等待60秒后運(yùn)行到System.out.println("111");方法。

所以,調(diào)用wait(long l)方法不僅達(dá)到了阻塞當(dāng)前線程規(guī)定時(shí)間內(nèi)不運(yùn)行,而且讓其它有競(jìng)爭(zhēng)需求的線程有了運(yùn)行機(jī)會(huì),這種利人不損己的方法,何樂(lè)而不為?這也是一個(gè)有良心的程序員應(yīng)該遵循的原則。
當(dāng)一個(gè)線程調(diào)用wait(long l)方法后,線程如果繼續(xù)運(yùn)行,你無(wú)法知道它是等待時(shí)間完成了還是在wait時(shí)被其它線程喚醒了,如果你非常在意它一定要等待足夠的時(shí)間才執(zhí)行某任務(wù),而不希望是中途被喚醒,這里有一個(gè)不是非常準(zhǔn)確的方法:

long l = System.System.currentTimeMillis();
wait(
1000);//準(zhǔn)備讓當(dāng)前線程等待1秒
while((System.System.currentTimeMillis() - l) < 1000)//執(zhí)行到這里說(shuō)明它還沒(méi)有等待到1秒
                             
//是讓其它線程給鬧醒了
    wait(1000-(System.System.currentTimeMillis()-l));//繼續(xù)等待余下的時(shí)間.  
    
//這種方法不是很準(zhǔn)確,但基本上能達(dá)到目的。


所以在同步方法中,除非你明確知道自己在干什么,非要這么做的話,你沒(méi)有理由使用sleep,wait方法足夠達(dá)到你想要的目的。而如果你是一個(gè)很保守的人,看到上面這段話后,你對(duì)sleep方法深惡痛絕,堅(jiān)決不用sleep了,那么在非同步的方法中(沒(méi)有和其它線程競(jìng)爭(zhēng)的對(duì)象),你想讓當(dāng)前線程阻塞一定時(shí)間后再運(yùn)行,應(yīng)該如何做呢?(這完全是一種賣弄,在非同步的方法中你就應(yīng)該合理地應(yīng)用sleep嘛,但如果你堅(jiān)決不用sleep,那就這樣來(lái)做吧)

public static mySleep(long l){
        Object o 
= new Object();
        
synchronized(o){
            
try{
                o.wait(l);    
            }
catch(Exception e){}
        }

    }
 

     放心吧,沒(méi)有人能在這個(gè)方法外調(diào)用o.notify[All],所以o.wait(l)會(huì)一直等到設(shè)定的時(shí)間才會(huì)運(yùn)行完成。

 

[虛擬鎖的使用]

  虛擬鎖簡(jiǎn)單說(shuō)就是不要調(diào)用synchronized方法(它等同于synchronized(this))和不要調(diào)用synchronized(this),這樣所有調(diào)用在這個(gè)實(shí)例上的所有同步方法的線程只能有一個(gè)線程可以運(yùn)行。也就是說(shuō):

  如果一個(gè)類有兩個(gè)同步方法 m1,m2,那么不僅是兩個(gè)以上線調(diào)用m1方法的線程只有一個(gè)能運(yùn)行,就是兩個(gè)分別調(diào)用m1,m2的線程也只有一個(gè)能運(yùn)行。當(dāng)然非同步方法不存在任何競(jìng)爭(zhēng),在一個(gè)線程獲取該對(duì)象的監(jiān)視鎖后這個(gè)對(duì)象的非同步方法可以被任何線程調(diào)用。

  而大多數(shù)時(shí)候,我們可能會(huì)出現(xiàn)這種情況,多個(gè)線程調(diào)用m1時(shí)需要保護(hù)一種資源,而多個(gè)線程調(diào)用M2時(shí)要保護(hù)的是另一種資源,如果我們把m1,m2都設(shè)成同步方法。兩個(gè)分別調(diào)用這兩個(gè)方法的線程其實(shí)并不產(chǎn)生沖突,但它們都要獲取這個(gè)實(shí)例的鎖(同步方法是同步this)而產(chǎn)生了不必要競(jìng)爭(zhēng)。

  所以這里應(yīng)該采用虛擬鎖。

  即將m1和m2方法中各自保護(hù)的對(duì)象作為屬性a1,a2傳進(jìn)來(lái),然后將同步方法改為方法的同步塊分別以a1,a2為參數(shù),這樣到少是不同線程調(diào)用這兩個(gè)不同方法時(shí)不會(huì)產(chǎn)生競(jìng)爭(zhēng),當(dāng)然如果m1,m2方法都操作同一受保護(hù)對(duì)象則兩個(gè)方法還是應(yīng)該作為同步方法。這也是應(yīng)該將方法同步還是采用同步塊的理由之一

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();
    }

}
 

這個(gè)例子可以看到兩個(gè)線程分別調(diào)用st實(shí)例的m1和m2方法卻因?yàn)槎家@取st的監(jiān)視鎖而產(chǎn)生了競(jìng)爭(zhēng)。T2實(shí)例要在T1運(yùn)行完成后才能運(yùn)行(間隔了10秒)。而假設(shè)m1方法要操作操作一個(gè)文件 f1,m2方法要操作一個(gè)文件f2,當(dāng)然我們可以在方法中分別同步f1,f2,但現(xiàn)在還不知道f2,f2是否存在,如果不存在我們就同步了一個(gè)null對(duì)象,那么我們可以使用虛擬鎖:

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();
    }

}
 

我們看到兩個(gè)分別調(diào)用m1和m2的線程由于它們獲取不同對(duì)象的監(jiān)視鎖,它們沒(méi)有任何競(jìng)爭(zhēng)就正常運(yùn)行,只有這兩個(gè)線程同時(shí)調(diào)用m1或m2才會(huì)產(chǎn)生阻塞。

[深入了解線程對(duì)象與線程,線程與運(yùn)行環(huán)境]

  在基礎(chǔ)篇中的第一節(jié),我就強(qiáng)調(diào)過(guò),要了解多線程編程,首要的兩個(gè)概念就是線程對(duì)象和線程。現(xiàn)在我們來(lái)深入理解線程對(duì)象,線程,運(yùn)行環(huán)境之間的關(guān)系,弄清Runnable與Thread的作用。

  在JAVA平臺(tái)中,序列化機(jī)制是一個(gè)非常重要的機(jī)制,如果不能理解并熟練應(yīng)用序列化機(jī)制,你就不能稱得上一個(gè)java程序員。

  在JAVA平臺(tái)中,為什么有些對(duì)象中可序列化的,而有些對(duì)象就不能序列化?
  能序列化的對(duì)象,簡(jiǎn)單說(shuō)是一種可以復(fù)制(意味著可以按一定機(jī)制進(jìn)行重構(gòu)它)的對(duì)象,這種對(duì)象說(shuō)到底就是內(nèi)存中一些數(shù)據(jù)的組合。只要按一定位置和順序組合就能完整反映這個(gè)對(duì)象。而有些對(duì)象,是和當(dāng)前環(huán)境相關(guān)的,它反映了當(dāng)前運(yùn)行的環(huán)境和時(shí)序,所以不能被序列,否則在另外的環(huán)境和時(shí)序中就無(wú)法“還原”。

     比如,一個(gè)Socket對(duì)象:

     Socket sc = new Socket("111.111.111.111",80); 

    這個(gè)sc對(duì)象表示當(dāng)前正在運(yùn)行這段代碼的主機(jī)和IP為"111.111.111.111"的80端口之間建立的一個(gè)物理連結(jié),如果它被序列化,那么在
另一個(gè)時(shí)刻在另一個(gè)主機(jī)上它如何能被還原?Socket連結(jié)一旦斷開(kāi),就已經(jīng)不存在,它不可能在另一個(gè)時(shí)間被另一個(gè)主機(jī)所重現(xiàn)。重現(xiàn)的已經(jīng)不是原來(lái)那個(gè)sc對(duì)象了。

  線程對(duì)象也是這種不可序列化對(duì)象,當(dāng)我們new Thread時(shí),已經(jīng)初始化了當(dāng)前這個(gè)線程對(duì)象所在有主機(jī)的運(yùn)行環(huán)境相關(guān)的信息,線程調(diào)度機(jī)制,安全機(jī)制等只特定于當(dāng)前運(yùn)行環(huán)境的信息,假如它被序列化,在另一個(gè)環(huán)境中運(yùn)行的時(shí)候原來(lái)初始化的運(yùn)行環(huán)境的信息就不可能在新的環(huán)境中運(yùn)行。而假如要重新初始化,那它已經(jīng)不是原來(lái)那個(gè)線程對(duì)象了。

  正如Socket封裝了兩個(gè)主機(jī)之間的連結(jié),但它們并不是已經(jīng)連結(jié)關(guān)傳送數(shù)據(jù)了。要想傳送數(shù)據(jù),你還要getInputStream和getOutputStream,并read和write,兩臺(tái)主機(jī)之間才開(kāi)始真正的“數(shù)據(jù)連結(jié)”。

  一個(gè)Thread對(duì)象并建立后,只是有了可以"運(yùn)行"的令牌,僅僅只是一個(gè)"線程對(duì)象"。只有當(dāng)它調(diào)用start()后,當(dāng)前環(huán)境才會(huì)分配給它一個(gè)運(yùn)行的"空間",讓這段代碼開(kāi)始運(yùn)行。這個(gè)運(yùn)行的"空間",才叫真正的"線程"。也就是說(shuō),真正的線程是指當(dāng)前正在執(zhí)行的那一個(gè)"事件"。是那個(gè)線程對(duì)象所在的運(yùn)行環(huán)境。

  明白了上面的概念,我們?cè)賮?lái)看看JAVA中為什么要有Runnable對(duì)象和Thread對(duì)象。

  一、從設(shè)計(jì)技巧上說(shuō),JAVA中為了實(shí)現(xiàn)回調(diào),無(wú)法調(diào)用方法指針,那么利用接口來(lái)約束實(shí)現(xiàn)者強(qiáng)制提供匹配的方法,并將實(shí)現(xiàn)該接口的類的實(shí)例作為參數(shù)來(lái)提供給調(diào)用者,這是JAVA平臺(tái)實(shí)現(xiàn)回調(diào)的重要手段。

  二、但是從實(shí)際的操作來(lái)看,對(duì)于算法和數(shù)據(jù),是不依賴于任何環(huán)境的。所以把想要實(shí)現(xiàn)的操作中的算法和數(shù)據(jù)封裝到一個(gè)run方法中(由于算法本身是數(shù)據(jù)的一個(gè)部分,所以我把它們合并稱為數(shù)據(jù)),可以將離數(shù)據(jù)和環(huán)境的邏輯分離開(kāi)來(lái)。使程序員只關(guān)心如何實(shí)現(xiàn)我想做的操作,而不要關(guān)心它所在的環(huán)境。當(dāng)真正的需要運(yùn)行的時(shí)候再將這段"操作"傳給一個(gè)具體當(dāng)前環(huán)境的Thread對(duì)象。

  三、這是最最重要的原因:實(shí)現(xiàn)數(shù)據(jù)共享

 因?yàn)橐粋€(gè)線程對(duì)象不對(duì)多次運(yùn)行。所以把數(shù)據(jù)放在Thread對(duì)象中,不會(huì)被多個(gè)線程同時(shí)訪問(wèn)。簡(jiǎn)單說(shuō):

class T extends Thread{
        Object x;
        
public void run(){//;}
    }

    T t 
= new T();  //當(dāng)T的實(shí)例t運(yùn)行后,t所包含的數(shù)據(jù)x只能被一個(gè)t.start();對(duì)象共享,除非聲明成 static Object x;

  一個(gè)t的實(shí)例數(shù)據(jù)只能被一個(gè)線程訪問(wèn)。意思是"一個(gè)數(shù)據(jù)實(shí)例對(duì)應(yīng)一個(gè)線程"。
  而假如我們從外部傳入數(shù)據(jù),比如

  class T extends Thread{
        
private Object x;
        
public T(Object x){
            
this.x = x;
        }

        
public void run(){//;}
    }
  

  這樣我們就可以先生成一個(gè)x對(duì)象傳給多個(gè)Thread對(duì)象,多個(gè)線程共同操作一個(gè)數(shù)據(jù)。也就是"一個(gè)數(shù)據(jù)實(shí)例對(duì)應(yīng)多個(gè)線程"。

  現(xiàn)在我們把數(shù)據(jù)更好地組織一下,把要操作的數(shù)據(jù)Object x和要進(jìn)行的操作一個(gè)封裝到Runnable的run()方法中,把Runnable實(shí)例從外部傳給多個(gè)Thread對(duì)象。這樣,我們就有了:

  [一個(gè)對(duì)象的多個(gè)線程]
  這是以后我們要介紹的線程池的重要概念。


 

 [線程的中斷]

 不客氣地說(shuō),至少有一半人認(rèn)為,線程的“中斷”就是讓線程停止。如果你也這么認(rèn)為,那你對(duì)多線程編程還沒(méi)有入門。

  在java中,線程的中斷(interrupt)只是改變了線程的中斷狀態(tài),至于這個(gè)中斷狀態(tài)改變后帶來(lái)的結(jié)果,那是無(wú)法確定的,有時(shí)它更是讓停止中的線程繼續(xù)執(zhí)行的唯一手段。不但不是讓線程停止運(yùn)行,反而是繼續(xù)執(zhí)行線程的手段。

  對(duì)于執(zhí)行一般邏輯的線程,如果調(diào)用它的interrupt()方法,那么對(duì)這個(gè)線程沒(méi)有任何影響,比如線程a正在執(zhí)行:while(條件) x ++;這樣的語(yǔ)句,如果其它線程調(diào)用a.interrupt();那么并不會(huì)影響a對(duì)象上運(yùn)行的線程,如果在其它線程里測(cè)試a的中斷狀態(tài)它已經(jīng)改變,但并不會(huì)停止這個(gè)線程的運(yùn)行。在一個(gè)線程對(duì)象上調(diào)用interrupt()方法,真正有影響的是wait,join,sleep方法,當(dāng)然這三個(gè)方法包括它們的重載方法。
  請(qǐng)注意:[上面這三個(gè)方法都會(huì)拋出InterruptedException],記住這句話,下面我會(huì)重復(fù)。一個(gè)線程在調(diào)用interrupt()后,自己不會(huì)拋出InterruptedException異常,所以你看到interrupt()并沒(méi)有拋出這個(gè)異常,所以我上面說(shuō)如果線程a正在執(zhí)行while(條件) x ++;你調(diào)用a.interrupt();后線程會(huì)繼續(xù)正常地執(zhí)行下去。
  但是,如果一個(gè)線程被調(diào)用了interrupt()后,它的狀態(tài)是已中斷的。這個(gè)狀態(tài)對(duì)于正在執(zhí)行wait,join,sleep的線程,卻改變了線程的運(yùn)行結(jié)果。
  一、對(duì)于wait中等待notify/notifyAll喚醒的線程,其實(shí)這個(gè)線程已經(jīng)“暫停”執(zhí)行,因?yàn)樗谀骋粚?duì)象的休息室中,這時(shí)如果它的中斷狀態(tài)被改變,那么它就會(huì)拋出異常。這個(gè)InterruptedException異常不是線程拋出的,而是wait方法,也就是對(duì)象的wait方法內(nèi)部會(huì)不斷檢查在此對(duì)象上休息的線程的狀態(tài),如果發(fā)現(xiàn)哪個(gè)線程的狀態(tài)被置為已中斷,則會(huì)拋出InterruptedException,意思就是這個(gè)線程不能再等待了,其意義就等同于喚醒它了。
  這里唯一的區(qū)別是,被notify/All喚醒的線程會(huì)繼續(xù)執(zhí)行wait下面的語(yǔ)句,而在wait中被中斷的線程則將控制權(quán)交給了catch語(yǔ)句。一些正常的邏輯要被放到catch中來(lái)運(yùn)行。但有時(shí)這是唯一手段,比如一個(gè)線程a在某一對(duì)象b的wait中等待喚醒,其它線程必須獲取到對(duì)象b的監(jiān)視鎖才能調(diào)用b.notify()[All],否則你就無(wú)法喚醒線程a,但在任何線程中可以無(wú)條件地調(diào)用a.interrupt();來(lái)達(dá)到這個(gè)目的。只是喚醒后的邏輯你要放在catch中,當(dāng)然同notify/All一樣,繼續(xù)執(zhí)行a線程的條件還是要等拿到b對(duì)象的監(jiān)視鎖。

  二、對(duì)于sleep中的線程,如果你調(diào)用了Thread.sleep(一年);現(xiàn)在你后悔了,想讓它早些醒過(guò)來(lái),調(diào)用interrupt()方法就是唯一手段,只有改變它的中斷狀態(tài),讓它從sleep中將控制權(quán)轉(zhuǎn)到處理異常的catch語(yǔ)句中,然后再由catch中的處理轉(zhuǎn)換到正常的邏輯。同樣地,于join中的線程你也可以這樣處理。

  對(duì)于一般介紹多線程模式的書(shū)上,他們會(huì)這樣來(lái)介紹:當(dāng)一個(gè)線程被中斷后,在進(jìn)入wait,sleep,join方法時(shí)會(huì)拋出異常。是的,這一點(diǎn)也沒(méi)有錯(cuò),但是這有什么意義呢?如果你知道那個(gè)線程的狀態(tài)已經(jīng)處于中斷狀態(tài),為什么還要讓它進(jìn)入這三個(gè)方法呢?當(dāng)然有時(shí)是必須這么做的,但大多數(shù)時(shí)候沒(méi)有這么做的理由,所以我上面主要介紹了在已經(jīng)調(diào)用這三個(gè)方法的線程上調(diào)用interrupt()方法讓它從"暫停"狀態(tài)中恢復(fù)過(guò)來(lái)。這個(gè)恢復(fù)過(guò)來(lái)就可以包含兩個(gè)目的:

  一、[可以使線程繼續(xù)執(zhí)行],那就是在catch語(yǔ)句中招待醒來(lái)后的邏輯,或由catch語(yǔ)句轉(zhuǎn)回正常的邏輯。總之它是從wait,sleep,join的暫停狀態(tài)活過(guò)來(lái)了。

  二、[可以直接停止線程的運(yùn)行],當(dāng)然在catch中什么也不處理,或return,那么就完成了當(dāng)前線程的使命,可以使在上面“暫停”的狀態(tài)中立即真正的“停止”。


  [中斷線程]

 有了上一節(jié)[線程的中斷],我們就好進(jìn)行如何[中斷線程]了。這絕對(duì)不是玩一個(gè)文字游戲。是因?yàn)?#8220;線程的中斷”并不能保證“中斷線程”,所以我要特別地分為兩節(jié)來(lái)說(shuō)明。這里說(shuō)的“中斷線程”意思是“停止線程”,而為什么不用“停止線程”這個(gè)說(shuō)法呢?因?yàn)榫€程有一個(gè)明確的stop方法,但它是反對(duì)使用的,所以請(qǐng)大家記住,在java中以后不要提停止線程這個(gè)說(shuō)法,忘記它!但是,作為介紹線程知識(shí)的我,我仍然要告訴你為什么不用“停止線程”的理由。

 [停止線程]

  當(dāng)在一個(gè)線程對(duì)象上調(diào)用stop()方法時(shí),這個(gè)線程對(duì)象所運(yùn)行的線程就會(huì)立即停止,并拋出特殊的ThreadDeath()異常。這里的“立即”因?yàn)樘?#8220;立即”了,就象一個(gè)正在擺弄自己的玩具的孩子,聽(tīng)到大人說(shuō)快去睡覺(jué)去,就放著滿地的玩具立即睡覺(jué)去了。這樣的孩子是不乖的。假如一個(gè)線程正在執(zhí)行:

synchronized void {
 x 
= 3;
 y 
= 4;
}
 
由于方法是同步的,多個(gè)線程訪問(wèn)時(shí)總能保證x,y被同時(shí)賦值,而如果一個(gè)線程正在執(zhí)行到x = 3;時(shí),被調(diào)用了 stop()方法,即使在同步塊中,它也干脆地stop了,這樣就產(chǎn)生了不完整的殘廢數(shù)據(jù)。而多線程編程中最最基礎(chǔ)的條件要保證數(shù)據(jù)的完整性,所以請(qǐng)忘記線程的stop方法,以后我們?cè)僖膊灰f(shuō)“停止線程”了。

 

  結(jié)束一個(gè)線程,我們要分析線程的運(yùn)行情況。也就是線程正在干什么。如果那個(gè)孩子什么事也沒(méi)干,那就讓他立即去睡覺(jué)。而如果那個(gè)孩子正在擺弄他的玩具,我們就要讓它把玩具收拾好再睡覺(jué)。

  所以一個(gè)線程從運(yùn)行到真正的結(jié)束,應(yīng)該有三個(gè)階段:

 

正常運(yùn)行.
處理結(jié)束前的工作,也就是準(zhǔn)備結(jié)束.
結(jié)束退出.
  在我的JDBC專欄中我N次提醒在一個(gè)SQL邏輯結(jié)束后,無(wú)論如何要保證關(guān)閉Connnection那就是在finally從句中進(jìn)行。同樣,線程在結(jié)束前的工作應(yīng)該在finally中來(lái)保證線程退出前一定執(zhí)行:

try{
  正在邏輯
 }
catch(){}
 
finally{
  清理工作
 }
  那么如何讓一個(gè)線程結(jié)束呢?既然不能調(diào)用stop,可用的只的interrupt()方法。但interrupt()方法只是改變了線程的運(yùn)行狀態(tài),如何讓它退出運(yùn)行?對(duì)于一般邏輯,只要線程狀態(tài)已經(jīng)中斷,我們就可以讓它退出,所以這樣的語(yǔ)句可以保證線程在中斷后就能結(jié)束運(yùn)行:

while(!isInterrupted()){
  正常邏輯
 }
  這樣如果這個(gè)線程被調(diào)用interrupt()方法,isInterrupted()為true,就會(huì)退出運(yùn)行。但是如果線程正在執(zhí)行wait,sleep,join方法,你調(diào)用interrupt()方法,這個(gè)邏輯就不完全了。

  如果一個(gè)有經(jīng)驗(yàn)的程序員來(lái)處理線程的運(yùn)行的結(jié)束:

public void run(){
  
try{
   
while(!isInterrupted()){
    正常工作
   }

  }

  
catch(Exception e){
   
return;
  }

  
finally{
   清理工作
  }

  
 }
 

 

 我們看到,如果線程執(zhí)行一般邏輯在調(diào)用innterrupt后,isInterrupted()為true,退出循環(huán)后執(zhí)行清理工作后結(jié)束,即使線程正在wait,sleep,join,也會(huì)拋出異常執(zhí)行清理工作后退出。

  這看起來(lái)非常好,線程完全按最我們?cè)O(shè)定的思路在工作。但是,并不是每個(gè)程序員都有這種認(rèn)識(shí),如果他聰明的自己處理異常會(huì)如何?事實(shí)上很多或大多數(shù)程序員會(huì)這樣處理:

 

public void run(){
  
  
while(!isInterrupted()){
   
try{
    正常工作
   }
catch(Exception e){
    
//nothing
   }

   
finally{
   
   }

  }

 }
 
} 

 

 想一想,如果一個(gè)正在sleep的線程,在調(diào)用interrupt后,會(huì)如何?wait方法檢查到isInterrupted()為true,拋出異常,而你又沒(méi)有處理。而一個(gè)拋出了InterruptedException的線程的狀態(tài)馬上就會(huì)被置為非中斷狀態(tài),如果catch語(yǔ)句沒(méi)有處理異常,則下一次循環(huán)中isInterrupted()為false,線程會(huì)繼續(xù)執(zhí)行,可能你N次拋出異常,也無(wú)法讓線程停止。

  那么如何能確保線程真正停止?在線程同步的時(shí)候我們有一個(gè)叫“二次惰性檢測(cè)”(double check),能在提高效率的基礎(chǔ)上又確保線程真正中同步控制中。那么我把線程正確退出的方法稱為“雙重安全退出”,即不以isInterrupted()為循環(huán)條件。而以一個(gè)標(biāo)記作為循環(huán)條件:

 

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{
   
   }

  }

 }

}

試試這段程序,可以正確工作嗎?
對(duì)于這段程序仍然還有很多可說(shuō)的地方,先到這里吧。