天天看點

java多線程學習筆記——詳細

一、線程類 

  

java多線程學習筆記——詳細

    1、建立狀态(New):新建立了一個線程對象。

        2、就緒狀态(Runnable):線程對象建立後,其他線程調用了該對象的start()方法。該狀态的線程位于可運作線程池中,變得可運作,等待擷取CPU的使用權。

        3、運作狀态(Running):就緒狀态的線程擷取了CPU,執行程式代碼。

        4、阻塞狀态(Blocked):阻塞狀态是線程因為某種原因放棄CPU使用權,暫時停止運作。直到線程進入就緒狀态,才有機會轉到運作狀态。阻塞的情況分三種:

        (一)、等待阻塞:運作的線程執行wait()方法,JVM會把該線程放入等待池中。

        (二)、同步阻塞:運作的線程在擷取對象的同步鎖時,若該同步鎖被别的線程占用,則JVM會把該線程放入鎖池中。

        (三)、其他阻塞:運作的線程執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該線程置為阻塞狀态。當sleep()狀态逾時、join()等待線程終止或者逾時、或者                    I/O處理完畢時,線程重新轉入就緒狀态。

        5、死亡狀态(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命周期。

  線程排程:

  1、調整線程優先級:Java線程有優先級,優先級高的線程會獲得較多的運作機會。

                Java線程的優先級用整數表示,取值範圍是1~10,Thread類有以下三個靜态常量:

                        static int MAX_PRIORITY 

                                  線程可以具有的最高優先級,取值為10。 

                        static int MIN_PRIORITY 

                                  線程可以具有的最低優先級,取值為1。 

                        static int NORM_PRIORITY 

                                  配置設定給線程的預設優先級,取值為5。 

        Thread類的setPriority()和getPriority()方法分别用來設定和擷取線程的優先級。

        每個線程都有預設的優先級。主線程的預設優先級為Thread.NORM_PRIORITY。

        線程的優先級有繼承關系,比如A線程中建立了B線程,那麼B将和A具有相同的優先級。

        JVM提供了10個線程優先級,但與常見的作業系統都不能很好的映射。如果希望程式能移植到各個作業系統中,應該僅僅使用Thread類有以下三個靜态常量作為優先級,這樣能保證同樣的優先級采用了同樣的排程方式。

        2、線程睡眠:Thread.sleep(long millis)方法,使線程轉到阻塞狀态。millis參數設定睡眠的時間,以毫秒為機關。當睡眠結束後,就轉為就緒(Runnable)狀态。sleep()平台移植性好。

        3、線程等待:Object類中的wait()方法,導緻目前的線程等待,直到其他線程調用此對象的 notify() 方法或 notifyAll() 喚醒方法。這個兩個喚醒方法也是Object類中的方法,行為等價于調用 wait(0) 一樣。

        4、線程讓步:Thread.yield() 方法,暫停目前正在執行的線程對象,把執行機會讓給相同或者更高優先級的線程。

        5、線程加入:join()方法,等待其他線程終止。在目前線程中調用另一個線程的join()方法,則目前線程轉入阻塞狀态,直到另一個程序運作結束,目前線程再由阻塞轉為就緒狀态。

        6、線程喚醒:Object類中的notify()方法,喚醒在此對象螢幕上等待的單個線程。如果所有線程都在此對象上等待,則會選擇喚醒其中一個線程。選擇是任意性的,并在對實作做出決定時發生。線程通過調用其中一個 wait 方法,在對象的螢幕上等待。 直到目前的線程放棄此對象上的鎖定,才能繼續執行被喚醒的線程。被喚醒的線程将以正常方式與在該對象上主動同步的其他所有線程進行競争;例如,喚醒的線程在作為鎖定此對象的下一個線程方面沒有可靠的特權或劣勢。類似的方法還有一個notifyAll(),喚醒在此對象螢幕上等待的所有線程。

        注意:Thread中suspend()和resume()兩個方法在JDK1.5中已經廢除,不再介紹。因為有死鎖傾向。

        7、常見線程名詞解釋

        主線程:JVM調用程式mian()所産生的線程。

        目前線程:這個是容易混淆的概念。一般指通過Thread.currentThread()來擷取的程序。

        背景線程:指為其他線程提供服務的線程,也稱為守護線程。JVM的垃圾回收線程就是一個背景線程。

        前台線程:是指接受背景線程服務的線程,其實前台背景線程是聯系在一起,就像傀儡和幕後操縱者一樣的關系。傀儡是前台線程、幕後操縱者是背景線程。由前台線程建立的線程預設也是前台線程。可以通過isDaemon()和setDaemon()方法來判斷和設定一個線程是否為背景線程。

  Java是通過Java.lang.Thread類來實作多線程的,第個Thread對象描述了一個單獨的線程。要産生一個線程,有兩種方法: 

    1、需要從Java.lang.Thread類繼承一個新的線程類,重載它的run()方法; 

    2、通過Runnalbe接口實作一個從非線程類繼承來類的多線程,重載Runnalbe接口的run()方法。運作一個新的線程,隻需要調用它的start()方法即可。如:

/**=====================================================================
* 檔案:ThreadDemo_01.java
* 描述:産生一個新的線程
* ======================================================================
*/
class ThreadDemo extends Thread{
  
  // 重載run函數
  public void run()
  {
     for (int count = 1,row = 1; row < 20; row++,count++)
     {
        for (int i = 0; i < count; i++)
        {
           System.out.print('*');
        }
        System.out.println();
     }
  }
}

class ThreadMain{
  public static void main(String argv[]){
    ThreadDemo th = new ThreadDemo();
    // 調用start()方法執行一個新的線程
    th.start();
  }
}      

線程類的一些常用方法: 

  sleep(): 強迫一個線程睡眠N毫秒。 

  isAlive(): 判斷一個線程是否存活。 

  join(): 等待線程終止。 

  activeCount(): 程式中活躍的線程數。 

  enumerate(): 枚舉程式中的線程。 

    currentThread(): 得到目前線程。 

  isDaemon(): 一個線程是否為守護線程。 

  setDaemon(): 設定一個線程為守護線程。(使用者線程和守護線程的差別在于,是否等待主線程依賴于主線程結束而結束) 

  setName(): 為線程設定一個名稱。 

  wait(): 強迫一個線程等待。 

  notify(): 通知一個線程繼續運作。 

  setPriority(): 設定一個線程的優先級。 

二、等待一個線程的結束 

  有些時候我們需要等待一個線程終止後再運作我們的另一個線程,這時我們應該怎麼辦呢?請看下面的例子:

/**=====================================================================
* 檔案:ThreadDemo_02.java
* 描述:等待一個線程的結束
* ======================================================================
*/
class ThreadDemo extends Thread{

  
  // 重載run函數
  public void run()
  {
     for (int count = 1,row = 1; row < 20; row++,count++)
     {
        for (int i = 0; i < count; i++)
        {
           System.out.print('*');
        }
        System.out.println();
     }
  }
}

class ThreadMain{
  public static void main(String argv[]){
    //産生兩個同樣的線程
    ThreadDemo th1 = new ThreadDemo();
    ThreadDemo th2 = new ThreadDemo();

   // 我們的目的是先運作第一個線程,再運作第二個線程
   th1.start();
   th2.start();
  }
}      

這裡我們的目标是要先運作第一個線程,等第一個線程終止後再運作第二個線程,而實際運作的結果是如何的呢?實際上我們運作的結果并不是兩個我們想要的直角三角形,而是一些亂七八糟的*号行,有的長,有的短。為什麼會這樣呢?因為線程并沒有按照我們的調用順序來執行,而是産生了線程賽跑現象。實際上Java并不能按我們的調用順序來執行線程,這也說明了線程是并行執行的單獨代碼。如果要想得到我們預期的結果,這裡我們就需要判斷第一個線程是否已經終止,如果已經終止,再來調用第二個線程。代碼如下:

/**=====================================================================
* 檔案:ThreadDemo_03.java
* 描述:等待一個線程的結束的兩種方法
* ======================================================================
*/
class ThreadDemo extends Thread{

  
  // 重載run函數
  public void run()
  {
     for (int count = 1,row = 1; row < 20; row++,count++)
     {
        for (int i = 0; i < count; i++)
        {
           System.out.print('*');
        }
        System.out.println();
     }
  }
}

class ThreadMain{
  public static void main(String argv[]){
    ThreadMain test = new ThreadMain();
    test.Method1();
    // test.Method2();
  }

  // 第一種方法:不斷查詢第一個線程是否已經終止,如果沒有,則讓主線程睡眠一直到它終止為止
 // 即:while/isAlive/sleep
 public void Method1(){
    ThreadDemo th1 = new ThreadDemo();
    ThreadDemo th2 = new ThreadDemo();
    // 執行第一個線程
    th1.start();
    // 不斷查詢第一個線程的狀态
    while(th1.isAlive()){
      try{
         Thread.sleep(100);
      }catch(InterruptedException e){
      }
    }
    //第一個線程終止,運作第二個線程
    th2.start();
  }
  
  // 第二種方法:join()
  public void Method2(){
    ThreadDemo th1 = new ThreadDemo();
    ThreadDemo th2 = new ThreadDemo();
    // 執行第一個線程
    th1.start();
    try{
      th1.join();
    }catch(InterruptedException e){
    }
    // 執行第二個線程
  th2.start();
}      

三、線程的同步問題 

   有些時候,我們需要很多個線程共享一段代碼,比如一個私有成員或一個類中的靜态成員,但是由于線程賽跑的問題,是以我們得到的常常不是正确的輸出結果,而相反常常是張冠李戴,與我們預期的結果大不一樣。看下面的例子:

/**=============================================================================
 * 檔案:ThreadDemo_04.java
 * 描述:多線程不同步的原因
 * =============================================================================
 */
// 共享一個靜态資料對象
class ShareData{
 public static String szData = "";
}

class ThreadDemo extends Thread{
  
  private ShareData oShare;

  ThreadDemo(){
  }

  ThreadDemo(String szName,ShareData oShare){
    super(szName);
    this.oShare = oShare;
  }

  public void run(){
    // 為了更清楚地看到不正确的結果,這裡放一個大的循環
  for (int i = 0; i < 50; i++){
       if (this.getName().equals("Thread1")){
         oShare.szData = "這是第 1 個線程";
         // 為了示範産生的問題,這裡設定一次睡眠
     try{
           Thread.sleep((int)Math.random() * 100);
         catch(InterruptedException e){
         }
         // 輸出結果
         System.out.println(this.getName() + ":" + oShare.szData);
       }else if (this.getName().equals("Thread2")){
         oShare.szData = "這是第 1 個線程";
         // 為了示範産生的問題,這裡設定一次睡眠
     try{
           Thread.sleep((int)Math.random() * 100);
         catch(InterruptedException e){
         }
         // 輸出結果
         System.out.println(this.getName() + ":" + oShare.szData);
       }
   }
}

class ThreadMain{
  public static void main(String argv[]){
    ShareData oShare = new ShareData();
    ThreadDemo th1 = new ThreadDemo("Thread1",oShare);
    ThreadDemo th2 = new ThreadDemo("Thread2",oShare);

    th1.start();
    th2.start();
  }
}      

由于線程的賽跑問題,是以輸出的結果往往是Thread1對應“這是第 2 個線程”,這樣與我們要輸出的結果是不同的。為了解決這種問題(錯誤),Java為我們提供了“鎖”的機制來實作線程的同步。鎖的機制要求每個線程在進入共享代碼之前都要取得鎖,否則不能進入,而退出共享代碼之前則釋放該鎖,這樣就防止了幾個或多個線程競争共享代碼的情況,進而解決了線程的不同步的問題。可以這樣說,在運作共享代碼時則是最多隻有一個線程進入,也就是和我們說的壟斷。鎖機制的實作方法,則是在共享代碼之前加入synchronized段,把共享代碼包含在synchronized段中。上述問題的解決方法為:

/**=============================================================================
 * 檔案:ThreadDemo_05.java
 * 描述:多線程不同步的解決方法--鎖
 * =============================================================================
 */
// 共享一個靜态資料對象
class ShareData{
 public static String szData = "";
}

class ThreadDemo extends Thread{
  
  private ShareData oShare;

  ThreadDemo(){
  }

  ThreadDemo(String szName,ShareData oShare){
    super(szName);
    this.oShare = oShare;
  }

  public void run(){
    // 為了更清楚地看到不正确的結果,這裡放一個大的循環
  for (int i = 0; i < 50; i++){
       if (this.getName().equals("Thread1")){
         // 鎖定oShare共享對象
         synchronized (oShare){
           oShare.szData = "這是第 1 個線程";
           // 為了示範産生的問題,這裡設定一次睡眠
       try{
             Thread.sleep((int)Math.random() * 100);
           catch(InterruptedException e){
           }
           // 輸出結果
           System.out.println(this.getName() + ":" + oShare.szData);
         }
       }else if (this.getName().equals("Thread2")){
         // 鎖定共享對象
         synchronized (oShare){
           oShare.szData = "這是第 1 個線程";
           // 為了示範産生的問題,這裡設定一次睡眠
       try{
             Thread.sleep((int)Math.random() * 100);
           catch(InterruptedException e){
           }
           // 輸出結果
           System.out.println(this.getName() + ":" + oShare.szData);
         }
       }
   }
}

class ThreadMain{
  public static void main(String argv[]){
    ShareData oShare = new ShareData();
    ThreadDemo th1 = new ThreadDemo("Thread1",oShare);
    ThreadDemo th2 = new ThreadDemo("Thread2",oShare);

    th1.start();
    th2.start();
  }
}      

由于過多的synchronized段将會影響程式的運作效率,是以引入了同步方法,同步方法的實作則是将共享代碼單獨寫在一個方法裡,在方法前加上synchronized關鍵字即可。 

  線上程同步時的兩個需要注意的問題: 

  1、無同步問題:即由于兩個或多個線程在進入共享代碼前,得到了不同的鎖而都進入共享代碼而造成。 

  2、死鎖問題:即由于兩個或多個線程都無法得到相應的鎖而造成的兩個線程都等待的現象。這種現象主要是因為互相嵌套的synchronized代碼段而造成,是以,在程式中盡可能少用嵌套的synchronized代碼段是防止線程死鎖的好方法。

Java多線程學習筆記(二) 

四、Java的等待通知機制 

  在有些時候,我們需要在幾個或多個線程中按照一定的秩序來共享一定的資源。例如生産者--消費者的關系,在這一對關系中實際情況總是先有生産者生産了産品後,消費者才有可能消費;又如在父--子關系中,總是先有父親,然後才能有兒子。然而在沒有引入等待通知機制前,我們得到的情況卻常常是錯誤的。這裡我引入《用線程獲得強大的功能》一文中的生産者--消費者的例子:

/* ==================================================================================
 * 檔案:ThreadDemo07.java
 * 描述:生産者--消費者
 * 注:其中的一些注釋是我根據自己的了解加注的
 * ==================================================================================
 */

// 共享的資料對象
 class ShareData{
  private char c;
  
  public void setShareChar(char c){
   this.c = c;
  }
  
  public char getShareChar(){
   return this.c;
  }
 }
 
 // 生産者線程
 class Producer extends Thread{
  
  private ShareData s;
  
  Producer(ShareData s){
   this.s = s;
  }
  
  public void run(){
   for (char ch = 'A'; ch <= 'Z'; ch++){
    try{
     Thread.sleep((int)Math.random() * 4000);
    }catch(InterruptedException e){}
    
    // 生産
    s.setShareChar(ch);
    System.out.println(ch + " producer by producer.");
   }
  }
 }
 
 // 消費者線程
 class Consumer extends Thread{
  
  private ShareData s;
  
  Consumer(ShareData s){
   this.s = s;
  }
  
  public void run(){
   char ch;
   
   do{
    try{
     Thread.sleep((int)Math.random() * 4000);
    }catch(InterruptedException e){}
    // 消費
    ch = s.getShareChar();
    System.out.println(ch + " consumer by consumer.");
   }while(ch != 'Z');
  }
 }

class Test{
 public static void main(String argv[]){
  ShareData s = new ShareData();
  new Consumer(s).start();
  new Producer(s).start();
 }
}      

在以上的程式中,模拟了生産者和消費者的關系,生産者在一個循環中不斷生産了從A-Z的共享資料,而消費者則不斷地消費生産者生産的A-Z的共享資料。我們開始已經說過,在這一對關系中,必須先有生産者生産,才能有消費者消費。但如果運作我們上面這個程式,結果卻出現了在生産者沒有生産之前,消費都就已經開始消費了或者是生産者生産了卻未能被消費者消費這種反常現象。為了解決這一問題,引入了等待通知(wait/notify)機制如下: 

  1、在生産者沒有生産之前,通知消費者等待;在生産者生産之後,馬上通知消費者消費。 

  2、在消費者消費了之後,通知生産者已經消費完,需要生産。 

下面修改以上的例子(源自《用線程獲得強大的功能》一文):

/* ==================================================================================
 * 檔案:ThreadDemo08.java
 * 描述:生産者--消費者
 * 注:其中的一些注釋是我根據自己的了解加注的
 * ==================================================================================
 */

class ShareData{
 
 private char c;
 // 通知變量
 private boolean writeable = true;

 // ------------------------------------------------------------------------- 
 // 需要注意的是:在調用wait()方法時,需要把它放到一個同步段裡,否則将會出現
 // "java.lang.IllegalMonitorStateException: current thread not owner"的異常。
 // -------------------------------------------------------------------------
 public synchronized void setShareChar(char c){
  if (!writeable){
   try{
    // 未消費等待
    wait();
   }catch(InterruptedException e){}
  }
  
  this.c = c;
  // 标記已經生産
  writeable = false;
  // 通知消費者已經生産,可以消費
  notify();
 }
 
 public synchronized char getShareChar(){
  if (writeable){
   try{
    // 未生産等待
    wait();
   }catch(InterruptedException e){}  
  }
  // 标記已經消費
  writeable = true;
  // 通知需要生産
  notify();
  return this.c;
 }
}

// 生産者線程
class Producer extends Thread{
 
 private ShareData s;
 
 Producer(ShareData s){
  this.s = s;
 }
 
 public void run(){
  for (char ch = 'A'; ch <= 'Z'; ch++){
   try{
    Thread.sleep((int)Math.random() * 400);
   }catch(InterruptedException e){}
   
   s.setShareChar(ch);
   System.out.println(ch + " producer by producer.");
  }
 }
}

// 消費者線程
class Consumer extends Thread{
 
 private ShareData s;
 
 Consumer(ShareData s){
  this.s = s;
 }
 
 public void run(){
  char ch;
  
  do{
   try{
    Thread.sleep((int)Math.random() * 400);
   }catch(InterruptedException e){}
  
   ch = s.getShareChar();
   System.out.println(ch + " consumer by consumer.**");
  }while (ch != 'Z');
 }
}

class Test{
 public static void main(String argv[]){
  ShareData s = new ShareData();
  new Consumer(s).start();
  new Producer(s).start();
 }
}      

在以上程式中,設定了一個通知變量,每次在生産者生産和消費者消費之前,都測試通知變量,檢查是否可以生産或消費。最開始設定通知變量為true,表示還未生産,在這時候,消費者需要消費,于時修改了通知變量,調用notify()發出通知。這時由于生産者得到通知,生産出第一個産品,修改通知變量,向消費者發出通知。這時如果生産者想要繼續生産,但因為檢測到通知變量為false,得知消費者還沒有生産,是以調用wait()進入等待狀态。是以,最後的結果,是生産者每生産一個,就通知消費者消費一個;消費者每消費一個,就通知生産者生産一個,是以不會出現未生産就消費或生産過剩的情況。 

五、線程的中斷 

  在很多時候,我們需要在一個線程中調控另一個線程,這時我們就要用到線程的中斷。用最簡單的話也許可以說它就相當于播放機中的暫停一樣,當第一次按下暫停時,播放器停止播放,再一次按下暫停時,繼續從剛才暫停的地方開始重新播放。而在Java中,這個暫停按鈕就是Interrupt()方法。在第一次調用interrupt()方法時,線程中斷;當再一次調用interrupt()方法時,線程繼續運作直到終止。這裡依然引用《用線程獲得強大功能》一文中的程式片斷,但為了更友善看到中斷的過程,我在原程式的基礎上作了些改進,程式如下:

/* ===================================================================================
 * 檔案:ThreadDemo09.java
 * 描述:線程的中斷
 * ===================================================================================
 */
class ThreadA extends Thread{
 
 private Thread thdOther;
 
 ThreadA(Thread thdOther){
  this.thdOther = thdOther;
 }
 
 public void run(){
  
  System.out.println(getName() + " 運作...");
  
  int sleepTime = (int)(Math.random() * 10000);
  System.out.println(getName() + " 睡眠 " + sleepTime
   + " 毫秒。");
  
  try{
   Thread.sleep(sleepTime);
  }catch(InterruptedException e){}
  
  System.out.println(getName() + " 覺醒,即将中斷線程 B。");
  // 中斷線程B,線程B暫停運作
  thdOther.interrupt();
 }
}

class ThreadB extends Thread{
 int count = 0;
 
 public void run(){
  
  System.out.println(getName() + " 運作...");
  
  while (!this.isInterrupted()){
   System.out.println(getName() + " 運作中 " + count++);
      
   try{  
    Thread.sleep(10);
   }catch(InterruptedException e){
    int sleepTime = (int)(Math.random() * 10000);
    System.out.println(getName() + " 睡眠" + sleepTime
     + " 毫秒。覺醒後立即運作直到終止。");
    
    try{
     Thread.sleep(sleepTime);
    }catch(InterruptedException m){}
    
    System.out.println(getName() + " 已經覺醒,運作終止...");
    // 重新設定标記,繼續運作 
    this.interrupt();
   }
  }
  
  System.out.println(getName() + " 終止。");  
 }
}

class Test{
 public static void main(String argv[]){
  ThreadB thdb = new ThreadB();
  thdb.setName("ThreadB");
  
  ThreadA thda = new ThreadA(thdb);
  thda.setName("ThreadA");
  
  thdb.start();
  thda.start();
 }
}
  運作以上程式,你可以清楚地看到中斷的過程。首先線程B開始運作,接着運作線程A,線上程A睡眠一段時間覺醒後,調用interrupt()方法中斷線程B,此是可能B正在睡眠,覺醒後掏出一個InterruptedException異常,執行其中的語句,為了更清楚地看到線程的中斷恢複,我在InterruptedException異常後增加了一次睡眠,當睡眠結束後,線程B調用自身的interrupt()方法恢複中斷,這時測試isInterrupt()傳回true,線程退出。
線程和程序(Threads and Processes)
第一個關鍵的系統級概念,究竟什麼是線程或者說究竟什麼是程序?她們其實就是作業系統内部的一種資料結構。
程序資料結構掌握着所有與記憶體相關的東西:全局位址空間、檔案句柄等等諸如此類的東西。當一個程序放棄執行(準确的說是放棄占有CPU),而被作業系統交換到硬碟上,使别的程序有機會運作的時候,在那個程序裡的所有資料也将被寫到硬碟上,甚至包括整個系統的核心(core memory)。可以這麼說,當你想到程序(process),就應該想到記憶體(memory) (程序 == 記憶體)。如上所述,切換程序的代價非常大,總有那麼一大堆的記憶體要移來移去。你必須用秒這個機關來計量程序切換(上下文切換),對于使用者來說秒意味着明顯的等待和硬碟燈的狂閃(對于作者的我,就意味着IBM龍騰3代的爛掉,5555555)。言歸正傳,對于Java而言,JVM就幾乎相當于一個程序(process),因為隻有程序才能擁有堆記憶體(heap,也就是我們平時用new操作符,分出來的記憶體空間)。
那麼線程是什麼呢?你可以把它看成“一段代碼的執行”---- 也就是一系列由JVM執行的二進制指令。這裡面沒有對象(Object)甚至沒有方法(Method)的概念。指令執行的序列可以重疊,并且并行的執行。後面,我會更加詳細的論述這個問題。但是請記住,線程是有序的指令,而不是方法(method)。
線程的資料結構,與程序相反,僅僅隻包括執行這些指令的資訊。它包含目前的運作上下文(context):如寄存器(register)的内容、目前指令的在運作引擎的指令流中的位置、儲存方法(methods)本地參數和變量的運作時堆棧。如果發生線程切換,OS隻需把寄存器的值壓進棧,然後把線程包含的資料結構放到某個類是清單(LIST)的地方;把另一個線程的資料從清單中取出,并且用棧裡的值重新設定寄存器。切換線程更加有效率,時間機關是毫秒。對于Java而言,一個線程可以看作是JVM的一個狀态。
運作時堆棧(也就是前面說的存儲本地變量和參數的地方)是線程資料結構一部分。這是因為多個線程,每一個都有自己的運作時堆棧,也就是說存儲在這裡面的資料是絕對線程安全(後面将會詳細解釋這個概念)的。因為可以肯定一個線程是無法修改另一個線程的系統級的資料結構的。也可以這麼說一個不通路堆記憶體的(隻讀寫堆棧記憶體)方法,是線程安全的(Thread Safe)。
線程安全和同步
線程安全,是指一個方法(method)可以在多線程的環境下安全的有效的通路程序級的資料(這些資料是與其他線程共享的)。事實上,線程安全是個很難達到的目标。
線程安全的核心概念就是同步,它保證多個線程:
同時開始執行,并行運作
不同時通路相同的對象執行個體
不同時執行同一段代碼
我将會在後面的章節,一一細訴這些問題。但現在還是讓我們來看看同步的一種經典的
實作方法——信号量。信号量是任何可以讓兩個線程為了同步它們的操作而互相通信的對象。Java也是通過信号量來實作線程間通信的。
不要被微軟的文檔所暗示的信号量僅僅是Dijksta提出的計數型信号量所迷惑。信号量其實包含任何可以用來同步的對象。
如果沒有synchronized關鍵字,就無法用JAVA實作信号量,但是僅僅隻依靠它也不足夠。我将會在後面為大家示範一種用Java實作的信号量。
同步的代價很高喲!
       同步(或者說信号量,随你喜歡啦)的一個很讓人頭痛的問題就是代價。考慮一下,下面的代碼:
Listing 1.2:
import java.util.*;
import java.text.NumberFormat;
class Synch
{
    private static long[ ] locking_time = new long[100];
    private static long[ ] not_locking_time = new long[100];
  private static final long ITERATIONS = 10000000;
    synchronized long locking     (long a, long b){return a + b;}
    long              not_locking (long a, long b){return a + b;}

      private void test( int id )
      {
       long start = System.currentTimeMillis(); 
       for(long i = ITERATIONS; --i >= 0 ;)
           {     locking(i,i);           }
       locking_time[id] = System.currentTimeMillis() - start;
       start = System.currentTimeMillis();
        for(long i = ITERATIONS; --i >= 0 ;)
        {     not_locking(i,i);           }
          not_locking_time[id] = System.currentTimeMillis() - start;
      }
       static void print_results( int id )
       {     NumberFormat compositor = NumberFormat.getInstance();
             compositor.setMaximumFractionDigits( 2 );
             double time_in_synchronization = locking_time[id] - not_locking_time[id];
        System.out.println( "Pass " + id + ": Time lost: "
                       + compositor.format( time_in_synchronization                         )
                      + " ms. "
                       + compositor.format( ((double)locking_time[id]/not_locking_time[id])*100.0 )
                        + "% increase"                       );
       }
    static public void main(String[ ] args) throws InterruptedException
   {
             final Synch tester = new Synch();
             tester.test(0); print_results(0);
             tester.test(1); print_results(1);
             tester.test(2); print_results(2);
             tester.test(3); print_results(3);
             tester.test(4); print_results(4);
             tester.test(5); print_results(5);
             tester.test(6); print_results(6);
           final Object start_gate = new Object();
              Thread t1 = new Thread()
              {     public void run()
                     {     try{ synchronized(start_gate) {     start_gate.wait(); } }
                            catch( InterruptedException e ){}
                             tester.test(7);
                     }
              };
              Thread t2 = new Thread()
              {     public void run()
                     {     try{ synchronized(start_gate) {     start_gate.wait(); } }
                            catch( InterruptedException e ){}
                             tester.test(8);
                     }
              };
              Thread.currentThread().setPriority( Thread.MIN_PRIORITY );
              t1.start();
              t2.start();
              synchronized(start_gate){ start_gate.notifyAll(); }
              t1.join();
              t2.join();
              print_results( 7 );
              print_results( 8 );
    }
}      

運作以上程式,你可以清楚地看到中斷的過程。首先線程B開始運作,接着運作線程A,線上程A睡眠一段時間覺醒後,調用interrupt()方法中斷線程B,此是可能B正在睡眠,覺醒後掏出一個InterruptedException異常,執行其中的語句,為了更清楚地看到線程的中斷恢複,我在InterruptedException異常後增加了一次睡眠,當睡眠結束後,線程B調用自身的interrupt()方法恢複中斷,這時測試isInterrupt()傳回true,線程退出。