java多線程系列----------- 基本概念釋疑

一、線程和線程對象

        線程,是指正在執行的一個指令序列.在java平臺上是指從一個線程對象的start()開始.運行run方法體中的那一段相對獨立的過程.
        線程對象是可以產生線程的對象.比如在java平臺中Thread對象,Runnable對象.

二、併發與並行

        在單CPU系統中,系統調度在某一時刻只能讓一個線程運行,雖然這種調試機制有多種形式(大多數是時間片輪巡爲主),但無論如何,要通過不斷切換需要運行的線程讓其運行的方式就叫併發(concurrent).
        而在多CPU系統中,可以讓兩個以上的線程同時運行,這種可以同時讓兩個以上線程同時運行的方式叫做並行(parallel).   

三、線程類的幾個常用方法

1、interrupt()、interrupted()和isInterrupted()方法

        這三個方法是關係非常密切而且又比較複雜的,雖然它們各自的功能很清楚,但它們之間的關係有大多數人不是真正的瞭解。
        先說interrupt()方法,它是實例方法,而它也是最奇怪的方法,直到現在它的語義也不像它的名字那樣準確。大多數人以爲,一個線程象調用了interrupt()方法,那它對應的線程就應該被中斷而拋出異常,事實中,當一個線程對象調用interrupt()方法,它對應的線程並沒有被中斷,只是改變了它的中斷狀態,使當前線程的狀態變爲中斷狀態,如果沒有其它影響,線程還會自己繼續執行,只有當線程執行到sleep,wait,join等方法時,或者自己檢查中斷狀態而拋出異常的情況下,線程纔會拋出異常。
        如果線程對象調用interrupt()後它對應的線程就立即中斷,那麼interrupted()方法就不可能執行。因爲interrupted()方法是一個static方法,就是說只能在當前線程上調用,而如果一個線程interrupt()後它已經中斷了,那它又如何讓自己interrupted()?正因爲一個線程調用interrupt()後只是改變了中斷狀態,它可以繼續執行下去,在沒有調用sleep,wait,join等方法或自己拋出異常之前,它就可以調用interrupted()來清除中斷狀態(還會原狀)。
    interrupted()方法會檢查當前線程的中斷狀態,如果爲"被中斷狀態"則改變當前線程爲"非中斷狀態"並返回true,如果爲"非中斷狀態"則返回false,它不僅檢查當前線程是否爲中斷狀態,而且在保證當前線程回到非中斷狀態,所以它叫"interrupted",是說中斷的狀態已經結束(到非中斷狀態了)。
    isInterrupted()方法則僅僅檢查線程對象對應的線程是否是中斷狀態,並不改變它的狀態。

2、sleep()、join()和yield()方法

        sleep()方法是類方法,也就是對當前線程而言的,程序員不能指定某個線程去sleep,只能是當前線程執行到sleep()方法時,睡眠指定的時間(讓其它線程運行)。事實上也只能是類方法,在當前線程上調用。試想如果你調用一個線程對象的sleep()方法,那麼這個對象對應的線程如果不是正在運行,它如何sleep()?所以只有當前線程,因爲它正在執行,你才能保證它可以調用sleep()方法。
        原則:在同步方法中儘量不要調用線程的sleep()方法,或者簡單說,對於一般水平的程序員你基本不應該調用sleep()方法。
        join()方法:在線程對象a上調用join()方法,就是讓當前正在執行的線程等待線程對象a對應的線程運行完成後才繼續運行。
        yield()方法也是類方法,只在當前線程上調用,理由同上,它主是讓當前線程放棄本次分配到的時間片。
        原則:不是非常必要的情況下,沒有理由調用它].調用這個方法不會提高任何效率,只是降低了CPU的總週期。

3、wait()、notify()/notityAll()方法

        wait()、notify()/notityAll()方法是普通對象的方法(Object超類中實現),而不是線程對象的方法。
        wait(),notify()/notityAll()方法只能在同步方法中調用。
    class Test{
        public synchronized 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的休息室。這兩個線程只能等待其它線程調用notify()/notityAll()來喚醒. 但是如果調用的是有參數的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 synchronized void test(){
      if(x==0)
        try{
          wait();
        }catch(Exception e){}
    }
    public static void main(String[] args) throws Exception{
      new MyThread().start();
    }
}
我們就可以看到線程一直等待,注意這個線程進入等待後沒有其它線程喚醒,除非強行退出JVM環境,否則它一直等待。
所以請記住:線程要想調用一個對象的wait()方法就要先獲得該對象的監視鎖,而一旦調用wait()後又立即釋放該鎖。

四、線程的互斥控制

        多個線程同時操作某一對象時,一個線程對該對象的操作可能會改變其狀態,而該狀態會影響另一線程對該對象的真正結果。所以需要一種機制來管理這類問題的發生,當某個線程正在執行一個不可分割的部分時,其它線程不能不能同時執行這一部分。像這種控制某一時刻只能有一個線程執行某個執行單元的機制就叫互斥控制或共享互斥(mutual exclusion)
        在JAVA中,用synchronized關鍵字來實現互斥控制(JDK1.5已經發展了新的機制), 把一個單元聲明爲synchronized,就可以讓在同一時間只有一個線程操作該方法。
        每個對象只有一把監視鎖(monitor lock),一次只能被一個線程獲取。當一個線程獲取了這一個鎖後,其它線程就只能等待這個線程釋放鎖才能再獲取。
        那麼synchronized關鍵字到底鎖什麼?得到了誰的鎖? 對於同步塊,synchronized獲取的是參數中的對象鎖:
synchronized(obj){
     //...............
}
       線程執行到這裏時,首先要獲取obj這個實例的鎖,如果沒有獲取到只能等待。如果多個線程執行到這裏,只能有一個線程獲取obj的鎖,然後執行{}中的語句,所以,obj對象的作用範圍不同,控制程序不同。假如:
    public void test(){
        Object o = new Object();
        
        synchronized(obj){
            //...............
        }
    }
        這段程序控制不了任何,多個線程之間執行到Object o = new Object();時會各自產生一個對象然後獲取這個對象有監視鎖,各自皆大歡喜地執行。 而如果是類的屬性:
    class Test{
        Object o = new Object();
        public void test(){


            synchronized(o){
                //...............
            }
        }
    }
        所有執行到Test實例的synchronized(o)的線程,只有一個線程可以獲取到監視鎖.
        有時我們會這樣:
        public void test(){
            synchronized(this){
                //...............
            }
        }
        那麼所有執行Test實例的線程只能有一個線程執行。而synchornized(o)和synchronized(this)的範圍是不同的,因爲執行到Test實例的synchornized(o)的線程等待時,其它線程可以執行Test實例的synchronized(o1)部分,但多個線程同時只有一個可以執行Test實例的synchornized(this)。 而對於
    synchronized(Test.class){
          //...............
    }
這樣的同步塊而言,所有調用Test多個實例的線程只能有一個線程可以執行。
如果一個方法聲明爲synchronized的,則等同於在整個方法上調用synchronized(this)。
如果一個靜態方法被聲明爲synchronized,則等同於在整個方法上調用synchronized(類.class)。

四、線程中斷的理解

       設想這樣的情景:我們的應用在某一個時間段內,需要一個子線程不停的在後臺運行,這可能是一個下載過程,是一個對服務端socket的監聽,也可能是一個繪圖的計算過程。當我們想要終止線程的時候,我們會怎樣做呢?是設定一個標誌變量來控制跳出循環?還是使用thread.stop()?又或者是設置thread = null?
    有的時候我們需要一種規範的思路,使用規範的方法來解決一類問題。    
    我們首先要明白,線程終止的條件,有三種情況:
    1.當線程的run方法執行方法體中最後一條語句後。
    2.當執行retutrn語句返回時。
    3.當出現了在方法中沒有捕獲的異常時。
    在Java的早期版本中,還有一個stop方法,其他線程可以調用它終止線程,但是這個方法已經被棄用了,所以還在用的同學就不要繼續用了。
    我們的正確思路是,使用interrupt方法來終止我們的線程。
    首先要理解interrupt方法做了什麼:每一個線程都有一箇中斷狀態,這是一個boolean標誌,當線程調用了interrupt方法時,這個中斷狀態就會被置位。如果我們要檢查中斷狀態,可以使用Thread.currentThread().isInterrupted()來獲得是否中斷。
    但是如果線程被阻塞了(sleep or wait),當我們調用了interrupt方法的時候,就會產生InterruptedException異常。這是我們可以利用的地方。
    同樣的,如果中斷狀態先被置位了,然後我們調用了sleep方法,線程不會休眠,相反,它將清除中斷狀態,然後拋出InterruptedException。
    我們調用了interrupt並不意味着線程會終止,線程是否會終止,以及會如何繼續,是程序員來控制的。
    在本文中我們將會討論終止線程的規範用法,首先我們來看兩種情形的後臺線程寫法:
public void run()
{
    try{
        ...
        while(!Thread.currentThread.isInterrupted() && more work to do)
        {
            do more work
        }
    }
    catch(InterruptedException)
    {
        //thread was interrupted during sleep or wait
    }
    finally
    {
        cleanup, if required
    }
    //exiting the run method terminates the thread
}

public void run()
{
    try{
        ...
        while( more work to do)
        {
            do more work
            Thread.sleep(delay);
        }
    }
    catch(InterruptedException)
    {
        //thread was interrupted during sleep or wait
    }
    finally
    {
        cleanup, if required
    }
    //exiting the run method terminates the thread
}
    第一種寫法適用於後臺下載,文件拷貝以及類似情形,第二種寫法適合遊戲畫面刷新或者類似情形。
    第一種寫法利用了interrupt方法,作爲終止的請求,使得循環跳出,run方法執行完畢。而第二種方法則是利用當線程sleep的時候調用interrupt會拋出InterruptedException從而跳出了循環進而線程執行到結束。
    事實上這兩種寫法的區別就在於第二種使用了sleep。


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