Java 執行緒綜述

casularm發表於2004-10-16

寫具有多執行緒能力的程式經常會用到的方法有:

      run(), start(), wait(), notify(), notifyAll(), sleep(), yield(), join()

      還有一個重要的關鍵字:synchronized

      本文將對以上內容進行講解。

      一:run() 和start()

      示例1:

    public class ThreadTest extends Thread {
  public void run() {
    for (int i = 0; i < 10; i++) {
      System.out.print(" " + i);
    }
  }

  public static void main(String[] args) {
    new ThreadTest().start();
    new ThreadTest().start();
  }
}

  這是個簡單的多執行緒程式。run() 和start() 是大家都很熟悉的兩個方法。把希望並行處理的程式碼都放在run() 中;stat() 用於自動呼叫run(),
    這是JAVA的內在機制規定的。並且run() 的訪問控制符必須是public,返回值必須是void(這種說法不準確,run() 沒有返回值),run()
    不帶引數。

      這些規定想必大家都早已知道了,但你是否清楚為什麼run方法必須宣告成這樣的形式?這涉及到JAVA的方法覆蓋和過載的規定。這些內容很重要,
    請讀者參考相關資料。

      二:關鍵字synchronized

      有了synchronized關鍵字,多執行緒程式的執行結果將變得可以控制。synchronized關鍵字用於保護共享資料。請大家注意 "共享資料",
    你一定要分清哪些資料是共享資料,JAVA是物件導向的程式設計語言,所以初學者在編寫多執行緒程式時,容易分不清哪些資料是共享資料。請看下面的例子:

      示例2:

    public class ThreadTest implements Runnable {

  public synchronized void run() {
    for (int i = 0; i < 10; i++) {
      System.out.print(" " + i);
    }
  }

  public static void main(String[] args) {
    Runnable r1 = new ThreadTest();
    Runnable r2 = new ThreadTest();
    Thread t1 = new Thread(r1);
    Thread t2 = new Thread(r2);
    t1.start();
    t2.start();
  }
}

  在這個程式中,run() 被加上了synchronized關鍵字。在main方法中建立了兩個執行緒。你可能會認為此程式的執行結果一定為:0123456789
    0123456789。但你錯了!這個程式中synchronized關鍵字保護的不是共享資料(
    其實在這個程式中synchronized關鍵字沒有起到任何作用,此程式的執行結果是不可預先確定的)。這個程式中的t1, t2是兩個物件(r1,
    r2)的執行緒。JAVA是物件導向的程式設計語言,不同的物件的資料是不同的,r1,
    r2有各自的run() 方法,而synchronized使同一個物件的多個執行緒,
    在某個時刻只有其中的一個執行緒可以訪問這個物件的synchronized資料。每個物件都有一個 "鎖標誌",
    當這個物件的一個執行緒訪問這個物件的某個synchronized資料時,這個物件的所有被synchronized修飾的資料將被上鎖(因為 "鎖標誌"
    被當前執行緒拿走了),只有當前執行緒訪問完它要訪問的synchronized資料時,當前執行緒才會釋放 "鎖標誌",
    這樣同一個物件的其它執行緒才有機會訪問synchronized資料。

      示例3:

    public class ThreadTest implements Runnable {
  public synchronized void run() {
    for (int i = 0; i < 10; i++) {
      System.out.print(" " + i);
    }
  }

  public static void main(String[] args) {
    Runnable r = new ThreadTest();
    Thread t1 = new Thread(r);
    Thread t2 = new Thread(r);
    t1.start();

    t2.start();
  }
}

  如果你執行1000次這個程式,它的輸出結果也一定每次都是:01234567890123456789。因為這裡的synchronized保護的是共享資料。
    t1,
    t2是同一個物件(r)的兩個執行緒,當其中的一個執行緒(例如:t1)開始執行run() 方法時,由於run() 受synchronized保護,所以同一個物件的其他執行緒(
    t2)無法訪問synchronized方法(run方法)。只有當t1執行完後t2才有機會執行。

      示例4:

    public class ThreadTest implements Runnable {
  public void run() {
    synchronized (this) {
      for (int i = 0; i < 10; i++) {
        System.out.print(" " + i);
      }
    }
  }

  public static void main(String[] args) {
    Runnable r = new ThreadTest();
    Thread t1 = new Thread(r);
    Thread t2 = new Thread(r);
    t1.start();
    t2.start();
  }
}

  這個程式與示例3的執行結果一樣。在可能的情況下,應該把保護範圍縮到最小,可以用示例4的形式,this代表 "這個物件"。沒有必要把整個run() 保護起來,
    run() 中的程式碼只有一個for迴圈,所以只要保護for迴圈就可以了。

      示例5:

    public class ThreadTest implements Runnable {
  public void run() {
    for (int k = 0; k < 5; k++) {
      System.out.println(Thread.currentThread().getName()
                         + " : for loop : " + k);

    }
    synchronized (this) {
      for (int k = 0; k < 5; k++) {
        System.out.println(Thread.currentThread().getName()
                           + " : synchronized for loop : " + k);
      }
    }
  }

  public static void main(String[] args) {
    Runnable r = new ThreadTest();
    Thread t1 = new Thread(r, "t1_name");
    Thread t2 = new Thread(r, "t2_name");
    t1.start();
    t2.start();
  }
}

執行結果:t1_name : for loop : 0
    t1_name : for loop : 1
    t1_name : for loop : 2
    t2_name : for loop : 0
    t1_name : for loop : 3
    t2_name : for loop : 1
    t1_name : for loop : 4
    t2_name : for loop : 2
    t1_name : synchronized for loop : 0
    t2_name : for loop : 3
    t1_name : synchronized for loop : 1
    t2_name : for loop : 4
    t1_name : synchronized for loop : 2
    t1_name : synchronized for loop : 3
    t1_name : synchronized for loop : 4
    t2_name : synchronized for loop : 0
    t2_name : synchronized for loop : 1
    t2_name : synchronized for loop : 2
    t2_name : synchronized for loop : 3
    t2_name : synchronized for loop : 4

      第一個for迴圈沒有受synchronized保護。對於第一個for迴圈,t1,
    t2可以同時訪問。執行結果表明t1執行到了k = 2時,t2開始執行了。t1首先執行完了第一個for迴圈,此時還沒有執行完第一個for迴圈(
    t2剛執行到k = 2)。t1開始執行第二個for迴圈,當t1的第二個for迴圈執行到k = 1時,t2的第一個for迴圈執行完了。
    t2想開始執行第二個for迴圈,但由於t1首先執行了第二個for迴圈,這個物件的鎖標誌自然在t1手中(
    synchronized方法的執行權也就落到了t1手中),在t1沒執行完第二個for迴圈的時候,它是不會釋放鎖標誌的。
    所以t2必須等到t1執行完第二個for迴圈後,它才可以執行第二個for迴圈

三:sleep()

      示例6:

    public class ThreadTest implements Runnable {
  public void run() {
    for (int k = 0; k < 5; k++) {
      if (k == 2) {
        try {
          Thread.currentThread().sleep(5000);
        }
        catch (Exception e) {}
      }
      System.out.print(" " + k);
    }
  }

  public static void main(String[] args) {
    Runnable r = new ThreadTest();
    Thread t = new Thread(r);
    t.start();
  }
}

  sleep方法會使當前的執行緒暫停執行一定時間(給其它執行緒執行機會)。讀者可以執行示例6,看看結果就明白了。sleep方法會丟擲異常,必須提供捕獲程式碼。

      示例7:

    public class ThreadTest implements Runnable {
  public void run() {

    for (int k = 0; k < 5; k++) {
      if (k == 2) {
        try {
          Thread.currentThread().sleep(5000);
        }
        catch (Exception e) {}
      }
      System.out.println(Thread.currentThread().getName()
                         + " : " + k);
    }
  }

  public static void main(String[] args) {
    Runnable r = new ThreadTest();
    Thread t1 = new Thread(r, "t1_name");
    Thread t2 = new Thread(r, "t2_name");
    t1.setPriority(Thread.MAX_PRIORITY);
    t2.setPriority(Thread.MIN_PRIORITY);
    t1.start();
    t2.start();
  }
}

  t1被設定了最高的優先順序,t2被設定了最低的優先順序。t1不執行完,t2就沒有機會執行。但由於t1在執行的中途休息了5秒中,這使得t2就有機會執行了。
    讀者可以執行這個程式試試看。

      示例8:

    public class ThreadTest implements Runnable {
  public synchronized void run() {
    for (int k = 0; k < 5; k++) {
      if (k == 2) {
        try {
          Thread.currentThread().sleep(5000);
        }
        catch (Exception e) {}

      }
      System.out.println(Thread.currentThread().getName()
                         + " : " + k);
    }
  }

  public static void main(String[] args) {
    Runnable r = new ThreadTest();
    Thread t1 = new Thread(r, "t1_name");
    Thread t2 = new Thread(r, "t2_name");
    t1.start();
    t2.start();
  }
}

  請讀者首先執行示例8程式,從執行結果上看:一個執行緒在sleep的時候,並不會釋放這個物件的鎖標誌。

      四:join()

      示例9:

    public class ThreadTest implements Runnable {
  public static int a = 0;
  public void run() {
    for (int k = 0; k < 5; k++) {
      a = a + 1;
    }
  }

  public static void main(String[] args) {
    Runnable r = new ThreadTest();
    Thread t = new Thread(r);
    t.start();
    System.out.println(a);
  }
}

  請問程式的輸出結果是5嗎?答案是:有可能。其實你很難遇到輸出5的時候,通常情況下都不是5。這裡不講解為什麼輸出結果不是5,我要講的是:
    怎樣才能讓輸出結果為5!其實很簡單,join() 方法提供了這種功能。join() 方法,它能夠使呼叫該方法的執行緒在此之前執行完畢。

      把示例9的main() 方法該成如下這樣:

    public static void main(String[] args) throws Exception {
  Runnable r = new ThreadTest();
  Thread t = new Thread(r);
  t.start();
  t.join();
  System.out.println(a);
}

  這時,輸出結果肯定是5!join() 方法會丟擲異常,應該提供捕獲程式碼。或留給JDK捕獲。

      示例10:

    public class ThreadTest implements Runnable {
  public void run() {
    for (int k = 0; k < 10; k++) {
      System.out.print(" " + k);
    }
  }

  public static void main(String[] args) throws Exception {
    Runnable r = new ThreadTest();
    Thread t1 = new Thread(r);
    Thread t2 = new Thread(r);
    t1.start();
    t1.join();
    t2.start();
  }
}

  執行這個程式,看看結果是否與示例3一樣

五:yield()

      yield() 方法與sleep() 方法相似,只是它不能由使用者指定執行緒暫停多長時間。按照SUN的說法:
    sleep方法可以使低優先順序的執行緒得到執行的機會,當然也可以讓同優先順序和高優先順序的執行緒有執行的機會。而yield()
    方法只能使同優先順序的執行緒有執行的機會。

      示例11:

    public class ThreadTest implements Runnable {
  public void run() {
    8
        for (int k = 0; k < 10; k++) {
      if (k == 5 && Thread.currentThread().getName().equals("t1")) {
        Thread.yield();
      }
      System.out.println(Thread.currentThread().getName()
                         + " : " + k);
    }
  }

  public static void main(String[] args) {
    Runnable r = new ThreadTest();
    Thread t1 = new Thread(r, "t1");
    Thread t2 = new Thread(r, "t2");
    t1.setPriority(Thread.MAX_PRIORITY);
    t2.setPriority(Thread.MIN_PRIORITY);
    t1.start();
    t2.start();
  }
}

輸出結果:
    t1 : 0
    t1 : 1
    t1 : 2
    t1 : 3
    t1 : 4
    t1 : 5
    t1 : 6
    t1 : 7
    t1 : 8
    t1 : 9
    t2 : 0
    t2 : 1
    t2 : 2
    t2 : 3
    t2 : 4
    t2 : 5
    t2 : 6
    t2 : 7
    t2 : 8
    t2 : 9

      多次執行這個程式,輸出也是一樣。這說明:yield() 方法不會使不同優先順序的執行緒有執行的機會。

      六:wait(), notify(), notifyAll()

      首先說明:wait(), notify(),
    notifyAll() 這些方法由java.lang.Object類提供,而上面講到的方法都是由java.lang.Thread類提供(
    Thread類實現了Runnable介面)。

      wait(), notify(),
    notifyAll() 這三個方法用於協調多個執行緒對共享資料的存取,所以必須在synchronized語句塊內使用這三個方法。先看下面了例子:

      示例12:

    public class ThreadTest implements Runnable {
  public static int shareVar = 0;
  public synchronized void run() {
    if (shareVar == 0) {
      for (int i = 0; i < 10; i++) {
        shareVar++;
        if (shareVar == 5) {
          try {
            this.wait();
          }
          catch (Exception e) {}
        }
      }
    }
    if (shareVar != 0) {
      System.out.print(Thread.currentThread().getName());
      System.out.println(" shareVar = " + shareVar);
      this.notify();
    }
  }

  public static void main(String[] args) {
    Runnable r = new ThreadTest();
    Thread t1 = new Thread(r, "t1");
    10
        Thread t2 = new Thread(r, "t2");
    t1.start();
    t2.start();
  }
}

執行結果:
    t2 shareVar = 5
    t1 shareVar = 10

      t1執行緒最先執行。由於初始狀態下shareVar為0,t1將使shareVar連續加1,當shareVar的值為5時,t1呼叫wait() 方法,
    t1將處於休息狀態,同時釋放鎖標誌。這時t2得到了鎖標誌開始執行,shareVar的值已經變為5,所以t2直接輸出shareVar的值,
    然後再呼叫notify() 方法喚醒t1。t1接著上次休息前的進度繼續執行,把shareVar的值一直加到10,由於此刻shareVar的值不為0,
    所以t1將輸出此刻shareVar的值,然後再呼叫notify() 方法,由於此刻已經沒有等待鎖標誌的執行緒,所以此呼叫語句不起任何作用。

      這個程式簡單的示範了wait(), notify() 的用法,讀者還需要在實踐中繼續摸索。

      七:關於執行緒的補充

      編寫一個具有多執行緒能力的程式可以繼承Thread類,也可以實現Runnable介面。在這兩個方法中如何選擇呢?從物件導向的角度考慮,
    作者建議你實現Runnable介面。有時你也必須實現Runnable介面,例如當你編寫具有多執行緒能力的小應用程式的時候。

      執行緒的排程:NewRunningRunnableOtherwise BlockedDeadBlocked in object`sit()
    poolBlocked in object`slock poolnotify() Schedulercompletesrun() start()
    sleep() or join() sleep() timeout or thread join() s or interupt()
    Lockavailablesynchronized() Thread states

      terupt() 一個Thread物件在它的生命週期中會處於各種不同的狀態,上圖形象地說明了這點。wa in

      呼叫start() 方法使執行緒處於可執行狀態,這意味著它可以由JVM排程並執行。這並不意味著執行緒就會立即執行。

      實際上,程式中的多個執行緒並不是同時執行的。除非執行緒正在真正的多CPU計算機系統上執行,否則執行緒使用單CPU必須輪流執行。但是,由於這發生的很快,
    我們常常認為這些執行緒是同時執行的。

      JAVA執行時系統的計劃排程程式是搶佔性的。如果計劃排程程式正在執行一個執行緒並且來了另一個優先順序更高的執行緒,
    那麼當前正在執行的執行緒就被暫時終止而讓更高優先順序的執行緒執行。

      JAVA計劃排程程式不會為與當前執行緒具有同樣優先順序的另一個執行緒去搶佔當前的執行緒。但是,儘管計劃排程程式本身沒有時間片(
    即它沒有給相同優先順序的執行緒以執行用的時間片),但以Thread類為基礎的執行緒的系統實現可能會支援時間片分配。這依賴具體的作業系統,
    Windows與UNIX在這個問題上的支援不會完全一樣。

      由於你不能肯定小應用程式將執行在什麼作業系統上,因此你不應該編寫出依賴時間片分配的程式。就是說,
    應該使用yield方法以允許相同優先順序的執行緒有機會執行而不是希望每一個執行緒都自動得到一段CPU時間片。

      Thread類提供給你與系統無關的處理執行緒的機制。但是,執行緒的實際實現取決於JAVA執行所在的作業系統。因此,
    執行緒化的程式確實是利用了支援執行緒的作業系統。

      當建立執行緒時,可以賦予它優先順序。它的優先順序越高,它就越能影響執行系統。
    JAVA執行系統使用一個負責在所有執行JAVA程式內執行所有存在的計劃排程程式。
    該計劃排程程式實際上使用一個固定優先順序的演算法來保證每個程式中的最高優先順序的執行緒得到CPU--允許最高優先順序的執行緒在其它執行緒之前執行。

      對於在一個程式中有幾個相同優先順序的執行緒等待執行的情況,該計劃排程程式迴圈地選擇它們,當進行下一次選擇時選擇前面沒有執行的執行緒,
    具有相同優先順序的所有的執行緒都受到平等的對待。較低優先順序的執行緒在較高優先順序的執行緒已經死亡或者進入不可執行狀態之後才能執行。

      繼續討論wait(), notify(), notifyAll():

      當執行緒執行了對一個特定物件的wait() 呼叫時,那個執行緒被放到與那個物件相關的等待池中。此外,呼叫wait() 的執行緒自動釋放物件的鎖標誌。

      可以呼叫不同的wait():wait() 或wait(long timeout)

      對一個特定物件執行notify() 呼叫時,將從物件的等待池中移走一個任意的執行緒,並放到鎖標誌等待池中,那裡的執行緒一直在等待,
    直到可以獲得物件的鎖標誌。notifyAll() 方法將從物件等待池中移走所有等待那個物件的執行緒並放到鎖標誌等待池中。
    只有鎖標誌等待池中的執行緒能獲取物件的鎖標誌,鎖標誌允許執行緒從上次因呼叫wait() 而中斷的地方開始繼續執行。

      在許多實現了wait() / notify() 機制的系統中,醒來的執行緒必定是那個等待時間最長的執行緒。然而,在Java技術中,並不保證這點。

      注意,不管是否有執行緒在等待,都可以呼叫notify()。如果對一個物件呼叫notify() 方法,而在這個物件的鎖標誌等待池中並沒有執行緒,
    那麼notify() 呼叫將不起任何作用。

      在JAVA中,多執行緒是一個神奇的主題。之所以說它 "神奇",是因為多執行緒程式的執行結果不可預測,但我們又可以通過某些方法控制多執行緒程式的執行。
    要想靈活使用多執行緒,讀者還需要大量實踐。

      另外,從JDK 1.2開始,SUN就不建議使用resume(), stop(), suspend() 了

 

--------------------------------------------------------------------------------
import java.io.*;

//多執行緒程式設計
public class MultiThread {
  public static void main(String args[]) {
    System.out.println("我是主執行緒!");
//下面建立執行緒例項thread1
    ThreadUseExtends thread1 = new ThreadUseExtends();
//建立thread2時以實現了Runnable介面的THhreadUseRunnable類例項為引數
    Thread thread2 = new Thread(new ThreadUseRunnable(), "SecondThread");
    thread1.start(); //啟動執行緒thread1使之處於就緒狀態
//thread1.setPriority(6);//設定thread1的優先順序為6
//優先順序將決定cpu空出時,處於就緒狀態的執行緒誰先佔領cpu開始執行
//優先順序範圍1到10,MIN_PRIORITY,MAX_PRIORITY,NORM_PAIORITY
//新執行緒繼承建立她的父執行緒優先順序,父執行緒通常有普通優先順序即5NORM_PRIORITY
    System.out.println("主執行緒將掛起7秒!");
    try {
      Thread.sleep(7000); //主執行緒掛起7秒
    }
    catch (InterruptedException e) {
      return;
    }
    System.out.println("又回到了主執行緒!");
    if (thread1.isAlive()) {
      thread1.stop(); //如果thread1還存在則殺掉他
      System.out.println("thread1休眠過長,主執行緒殺掉了thread1!");
    }
    else
      System.out.println("主執行緒沒發現thread1,thread1已醒順序執行結束了!");
    thread2.start(); //啟動thread2
    System.out.println("主執行緒又將掛起7秒!");
    try {
      Thread.sleep(7000); //主執行緒掛起7秒
    }
    catch (InterruptedException e) {
      return;
    }
    System.out.println("又回到了主執行緒!");
    if (thread2.isAlive()) {
      thread2.stop(); //如果thread2還存在則殺掉他
      System.out.println("thread2休眠過長,主執行緒殺掉了thread2!");
    }
    else
      System.out.println("主執行緒沒發現thread2,thread2已醒順序執行結束了!");
    System.out.println("程式結束按任意鍵繼續!");
    try {
      System.in.read();
    }
    catch (IOException e) {
      System.out.println(e.toString());
    }

  } //main
} //MultiThread

class ThreadUseExtends
    extends Thread
//通過繼承Thread類,並實現它的抽象方法run()
//適當時候建立這一Thread子類的例項來實現多執行緒機制
//一個執行緒啟動後(也即進入就緒狀態)一旦獲得CPU將自動呼叫它的run()方法
{

  ThreadUseExtends() {} //建構函式

  public void run() {
    System.out.println("我是Thread子類的執行緒例項!");
    System.out.println("我將掛起10秒!");
    System.out.println("回到主執行緒,請稍等,剛才主執行緒掛起可能還沒醒過來!");
    try {
      sleep(10000); //掛起5秒
    }
    catch (InterruptedException e) {
      return;
    }
//如果該run()方法順序執行完了,執行緒將自動結束,而不會被主執行緒殺掉
//但如果休眠時間過長,則執行緒還存活,可能被stop()殺掉
  }
}

class ThreadUseRunnable
    implements Runnable
//通過實現Runnable介面中的run()方法,再以這個實現了run()方法的類
//為引數建立Thread的執行緒例項
{
//Thread thread2=new Thread(this);
//以這個實現了Runnable介面中run()方法的類為引數建立Thread類的執行緒例項
  ThreadUseRunnable() {} //建構函式

  public void run() {
    System.out.println("我是Thread類的執行緒例項並以實現了Runnable介面的類為引數!");
    System.out.println("我將掛起1秒!");
    System.out.println("回到主執行緒,請稍等,剛才主執行緒掛起可能還沒醒過來!");
    try {
      Thread.sleep(1000); //掛起5秒
    }
    catch (InterruptedException e) {
      return;
    }
//如果該run()方法順序執行完了,執行緒將自動結束,而不會被主執行緒殺掉
//但如果休眠時間過長,則執行緒還存活,可能被stop()殺掉
  }

}
//該程式可做的修改如改休眠時間或優先順序setPriority()

相關文章