【線程】

一個完整的例子來說明線程產生的方式不同而生成的線程的區別:

public class ThreadTest {
    /**
     * main(這裏用一句話描述這個方法的作用)
     * @param args
     * @Author:志會 
     * @Date: 2013-12-27上午8:46:52 
     */
    
    public static void main(String[] args) throws Exception{
        for(int i=0;i<10;i++){
          Thread t = new MyThread();
          t.start();
        }
          Thread.sleep(10000);//讓上面的線程運行完成
          R r = new R();
          for(int i=0;i<10;i++){
            Thread t = new Thread(r);
            t.start();
          }
        }   
    }

    class MyThread extends Thread{
        public int x = 0;
        public void run(){
          System.out.println(++x);
        }
    }
    class R implements Runnable{
        private int x = 0;
        public void run(){
          System.out.println(++x);
        }
    }


上面10個線程對象產生的10個線程運行時打印了10次1。下面10個線程對象產生的10個線程運行時打印了1到10。我們把下面的10個線程稱爲同一實例(Runnable實例)的多個線程

一個線程對象生成後,如果要產生一個執行的線程,就一定要調用它的start()方法.在介紹這個方法時不得不同時說明run方法.其實線程對 象的run方法完全是一個接口回調方法,它是你這個線程對象要完成的具體邏輯.簡單說你要做什麼就你在run中完成,而如何做,什麼時候做就不需要你控制 了,你只要調用start()方法,JVM就會管理這個線程對象讓它產生一個線程並註冊到線程處理系統中。

start()方法並沒有直接調用run方法

從中我們可以看到這個爲了控制一個線程對象只能運行成功一次start()方法.這是因爲線程的運行要獲取當前環境,包括安全,父線程的權限, 優先級等條件,如果一個線程對象可以運行多次,那麼定義一個static 的線程在一個環境中獲取相應權限和優先級,運行完成後它在另一個環境中利用原來的權限和優先級等屬性在當前環境中運行,這樣就造成無法預知的結果.簡單說 來,讓一個線程對象只能成功運行一次,是基於對線程管理的需要。

start()方法最本質的功能是從CPU中申請另一個線程空間來執行 run()方法中的代碼,它和當前的線程是兩條線,在相對獨立的線程空間運行,也就是說,如果你直接調用線程對象的run()方法,當然也會執行,但那是 在當前線程中執行,run()方法執行完成後繼續執行下面的代碼.而調用start()方法後,run()方法的代碼會和當前線程併發(單CPU)或並行 (多CPU)執行。

所以請記住一句話[調用線程對象的run方法不會產生一個新的線程],雖然可以達到相同的執行結果,但執行過程和執行效率不同。

[線程的interrupt()方法,interrupted()和isInterrupted()]

  這三個方法是關係非常密切而且又比較複雜的,雖然它們各自的功能很清楚,但它們之間的關係有大多數人不是真正的瞭解。

  先說interrupt()方法,它是實例方法,而它也是最奇怪的方法,在java語言中,線程最初被設計爲"隱晦難懂"的東西,直到現在它的 語義不沒有象它的名字那樣準確。大多數人以爲,一個線程象調用了interrupt()方法,那它對應的線程就應該被中斷而拋出異常,事實中,當一個線程 對象調用interrupt()方法,它對應的線程並沒有被中斷,只是改變了它的中斷狀態。

  使當前線程的狀態變以中斷狀態,如果沒有其它影響,線程還會自己繼續執行。

  只有當線程執行到sleep,wait,join等方法時,或者自己檢查中斷狀態而拋出異常的情況下,線程纔會拋出異常。
如果線程對象調用interrupt()後它對應的線程就立即中斷,那麼interrupted()方法就不可能執行。

   因爲interrupted()方法是一個static方法,就是說只能在當前線程上調用,而如果一個線程interrupt()後它已經中斷了,那它又如何讓自己interrupted()?

  正因爲一個線程調用interrupt()後只是改變了中斷狀態,它可以繼續執行下去,在沒有調用sleep,wait,join等法或自己拋 出異常之前,它就可以調用interrupted()來清除中斷狀態(還會原狀)interrupted()方法會檢查當前線程的中斷狀態,如果爲 "被中斷狀態"則改變當前線程爲"非中斷狀態"並返回true,如果爲"非中斷狀態"則返回false,它不僅檢查當前線程是否爲中斷狀態,而且在保證當 前線程回來非中斷狀態,所以它叫"interrupted",是說中斷的狀態已經結束(到非中斷狀態了)isInterrupted()方法則僅僅檢查線 程對象對應的線程是否是中斷狀態,並不改變它的狀態。

  目前大家只能先記住這三個方法的功能,只有真正深入到多線程編程實踐中,纔會體會到它們爲什麼是對象方法,爲什麼是類方法。


[sleep(),join(),yield()方法]

  在現在的環節中,我只能先說明這些方法的作用和調用原則,至於爲什麼,在基礎篇中無法深入,只能在提高篇中詳細說明。

  sleep()方法中是類方法,也就是對當前線程而言的,程序員不能指定某個線程去sleep,只能是當前線程執行到sleep()方法時,睡 眠指定的時間(讓其它線程運行).事實上也只能是類方法,在當前線程上調用.試想如果你調用一個線程對象的sleep()方法,那麼這個對象對應的線程如 果不是正在運行,它如何sleep()?所以只有當前線程,因爲它正在執行,你才能保證它可以調用sleep()方法。

  原則:[在同步方法中儘量不要調用線程的sleep()方法],或者簡單說,對於一般水平的程序員你基本不應該調用sleep()方法。

  join()方法,正如第一節所言,在一個線程對象上調用join方法,是當前線程等待這個線程對象對應的線程結束,比如有兩個工作,工作A要耗時10秒鐘,工作B要耗時10秒或更多。我們在程序中先生成一個線程去做工作B,然後做工作A。

  new?B().start();//做工作B

  A();//做工作A

  工作A完成後,下面要等待工作B的結果來進行處理.如果工作B還沒有完成我就不能進行下面的工作C,所以

  B?b?=?new?B();

  b.start();//做工作B

  A();//做工作A

  b.join();//等工作B完成。

  C();//繼續工作C。

  原則:[join是測試其它工作狀態的唯一正確方法],我見過很多人,甚至有的是博士生,在處理一項工作時如果另一項工作沒有完成,說讓當前工 作線程sleep(x),我問他,你這個x是如何指定的,你怎麼知道是100毫秒而不是99毫秒或是101毫秒?其實這就是OnXXX事件的實質,我們不 是要等多長時間纔去做什麼事,而是當等待的工作正好完成的時候去做。

  yield()方法也是類方法,只在當前線程上調用,理由同上,它主是讓當前線程放棄本次分配到的時間片原則:[不是非常必要的情況下,沒有理 由調用它].調用這個方法不會提高任何效率,只是降低了CPU的總週期上面介紹的線程一些方法,基於(基礎篇)而言只能簡單提及.以後具體應用中我會結合 實例詳細論述。


[wait(),notify()/notifyAll()]

  這是在多線程中非常重要的方法。

關於這兩個方法,有很多的內容需要說明.在下面的說明中可能會有很多地方不能一下子明白,但在看完本節後,即使不能完全明白,你也一定要回過頭來記住下面的兩句話:

  [wait(),notify()/notityAll()方法是普通對象的方法(Object超類中實現),而不是線程對象的方法]

  [wait(),notify()/notityAll()方法只能在同步方法中調用]


[線程的互斥控制]

  多個線程同時操作某一對象時,一個線程對該對象的操作可能會改變其狀態,而該狀態會影響另一線程對該對象的真正結果.

  這個例子我們在太多的文檔中可以看到,就象兩個操售票員同時售出同一張票一樣.

線程A 線程B
1.線程A在數據庫中查詢存票,發現票C可以賣出  
2.線程A接受用戶訂票請求,準備出票.  
  3.這時切換到了線程B執行
  4.線程B在數據庫中查詢存票,發現票C可以賣出
  5.線程B將票賣了出去
6.切換到線程A執行,線程A賣了一張已經賣出的票  

  所以需要一種機制來管理這類問題的發生,當某個線程正在執行一個不可分割的部分時,其它線程不能不能同時執行這一部分.

  象這種控制某一時刻只能有一個線程執行某個執行單元的機制就叫互斥控制或共享互斥(mutual exclusion)

  在JAVA中,用synchornized關鍵字來實現互斥控制(暫時這樣認爲,JDK1.5已經發展了新的機制)

[synchornized關鍵字]

  把一個單元聲明爲synchornized,就可以讓在同一時間只有一個線程操作該方法.

  有人說synchornized就是一把鎖,事實上它確實存在鎖,但是是誰的鎖,鎖誰,這是一個非常複雜的問題.

  每個對象只有一把監視鎖(monitor lock),一次只能被一個線程獲取.當一個線程獲取了這一個鎖後,其它線程就只能等待這個線程釋放鎖才能再獲取.
那麼synchornized關鍵字到底鎖什麼?得到了誰的鎖?
對於同步塊,synchornized獲取的是參數中的對象鎖:

synchornized(obj){
  //...............
  }

  線程執行到這裏時,首先要獲取obj這個實例的鎖,如果沒有獲取到線程只能等待.如果多個線程執行到這裏,只能有一個線程獲取obj的鎖,然後執行{}中的語句,所以,obj對象的作用範圍不同,控制程序不同.

  假如:

public void test(){
  Object o = new Object();
  synchornized(obj){
  //...............
  }
  }

  這段程序控制不了任何,多個線程之間執行到Object o = new Object();時會各自產生一個對象然後獲取這個對象有監視鎖,各自皆大歡喜地執行.

  而如果是類的屬性:

class Test{
    Object o = new Object();
    public void test(){
    synchornized(o){
    //...............
    }
    }
}

  所有執行到Test實例的synchornized(o)的線程,只有一個線程可以獲取到監視鎖.

  有時我們會這樣:

public void test(){
    synchornized(this){
    //...............
    }
    }

  那麼所有執行Test實例的線程只能有一個線程執行.而synchornized(o)和synchornized(this)的範圍是不同 的,因爲執行到Test實例的synchornized(o)的線程等待時,其它線程可以執行Test實例的synchornized(o1)部分,但多 個線程同時只有一個可以執行Test實例的synchornized(this).]

  而對於

synchornized(Test.class){
    //...............
    }

  這樣的同步塊而言,所有調用Test多個實例的線程賜教只能有一個線程可以執行.

[synchornized方法]

  如果一個方法聲明爲synchornized的,則等同於把在爲個方法上調用synchornized(this).

  如果一個靜態方法被聲明爲synchornized,則等同於把在爲個方法上調用synchornized(類.class).

  現在進入wait方法和notify/notifyAll方法.這兩個(或叫三個)方法都是Object對象的方法,而不是線程對象的方法.如同鎖一樣,它們是在線程中調用某一對象上執行的.

class Test{
    public synchornized void test(){
    //獲取條件,int x 要求大於100;

    if(x < 100)
    wait();
    }
    }

  這裏爲了說明方法沒有加在try{}catch(){}中,如果沒有明確在哪個對象上調用wait()方法,則爲this.wait();

  假如:

  Test t = new Test();

  現在有兩個線程都執行到t.test();方法.其中線程A獲取了t的對象鎖,進入test()方法內.

  這時x小於100,所以線程A進入等待.

  當一個線程調用了wait方法後,這個線程就進入了這個對象的休息室(waitset),這是一個虛擬的對象,但JVM中一定存在這樣的一個數據結構用來記錄當前對象中有哪些程線程在等待.

  當一個線程進入等待時,它就會釋放鎖,讓其它線程來獲取這個鎖.

  所以線程B有機會獲得了線程A釋放的鎖,進入test()方法,如果這時x還是小於100,線程B也進入了t的休息室.

  這兩個線程只能等待其它線程調用notity[All]來喚醒.

  但是如果調用的是有參數的wait(time)方法,則線程A,B都會在休息室中等待這個時間後自動喚醒.

[爲什麼真正的應用都是用while(條件)而不用if(條件)]

  在實際的編程中我們看到大量的例子都是用?

  while(x < 100)

  wait();go();而不是用if,爲什麼呢?

  在多個線程同時執行時,if(x <100)是不安全的.因爲如果線程A和線程B都在t的休息室中等待,這時另一個線程使x==100了,並調用notifyAll方法,線程A繼續 執行下面的go().而它執行完成後,x有可能又小於100,比如下面的程序中調用了--x,這時切換到線程B,線程B沒有繼續判斷,直接執行go(); 就產生一個錯誤的條件,只有while才能保證線程B又繼續檢查一次.

[notify/notifyAll方法]

  這兩個方法都是把某個對象上休息區內的線程喚醒,notify只能喚醒一個,但究竟是哪一個不能確定,而notifyAll則喚醒這個對象上的休息室中所有的線程.

  一般有爲了安全性,我們在絕對多數時候應該使用notifiAll(),除非你明確知道只喚醒其中的一個線程.

  那麼是否是隻要調用一個對象的wait()方法,當前線程就進入了這個對象的休息室呢?事實中,要調用一個對象的wait()方法,只有當前線程獲取了這個對象的鎖,換句話說一定要在這個對象的同步方法或以這個對象爲參數的同步塊中.

class MyThread extends Thread{
    Test t = new Test();
    public void run(){
    t.test();
    System.out.println("Thread say:Hello,World!");
    }
    }
 
    public class Test {

    int x = 0;
    public  void test(){
    if(x==0)
    try{
    wait();
    }catch(Exception e){}
    }
    public static void main(String[] args) throws Exception{
    new MyThread().start();
    }
    }

  這個線程就不會進入t的wait方法而直接打印出Thread say:Hello,World!.

  而如果改成:

public class Test {

      int x = 0;
      public synchornized void test(){
      if(x==0)
      try{
      wait();
      }catch(Exception e){}
      }
      public static void main(String[] args) throws Exception{
      new MyThread().start();
      }
      }

  我們就可以看到線程一直等待,注意這個線程進入等待後沒有其它線程喚醒,除非強行退出JVM環境,否則它一直等待.

  所以請記住:

  [線程要想調用一個對象的wait()方法就要先獲得該對象的監視鎖,而一旦調用wait()後又立即釋放該鎖]








發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章