Java多執行緒程式設計——進階篇一

九天高遠發表於2013-09-11

 一、執行緒棧模型與執行緒的變數

要理解執行緒排程的原理,以及執行緒執行過程,必須理解執行緒棧模型

執行緒棧是指某一時刻記憶體中執行緒排程的棧資訊,當前呼叫的方法總是位於棧頂。執行緒棧的內容是隨著程式的執行動態變化的,因此研究執行緒棧必須選擇一個執行的時刻(實際上指程式碼執行到什麼地方)。

 下面透過一個示例性的程式碼說明執行緒(呼叫)棧的變化過程

 

 

幅圖描述在程式碼執行到兩個不同時刻1、2時候,JVM虛擬機器執行緒呼叫棧示意圖。

當程式執行到t.start();時候,程式多出一個分支(增加了一個呼叫棧B),這樣,棧A、棧B並行執行

 從這裡就可以看出方法呼叫和執行緒啟動的區別了。

二、執行緒狀態的轉換

a、執行緒的狀態

執行緒的狀態轉換是執行緒控制的基礎。執行緒狀態總的可分為五大狀態:分別是生、死、可執行、執行、等待/阻塞。用下圖來描述如下:

 

 

1、新狀態:執行緒物件已經建立,還沒有在其上呼叫start()方法。

2、可執行狀態:當執行緒有資格執行,但排程程式還沒有把它選定為執行執行緒時執行緒所處的狀態。當start()方法呼叫時,執行緒首先進入可執行狀態。線上程執行之後或者從阻塞、等待或睡眠狀態回來後,也返回到可執行狀態。

3、執行狀態:執行緒排程程式從可執行池中選擇一個執行緒作為當前執行緒時執行緒所處的狀態。這也是執行緒進入執行狀態的唯一一種方式。

4、等待/阻塞/睡眠狀態:這是執行緒有資格執行時它所處的狀態。實際上這個三狀態組合為一種,其共同點是:執行緒仍舊是活的,但是當前沒有條件執行。換句話說,它是可執行的,但是如果某件事件出現,他可能返回到可執行狀態。

5、死亡態:當執行緒的run()方法完成時就認為它死去。這個執行緒物件也許是活的,但是,它已經不是一個單獨執行的執行緒。執行緒一旦死亡,就不能復生。 如果在一個死去的執行緒上呼叫start()方法,會丟擲java.lang.IllegalThreadStateException異常。

b、阻止執行緒執行

對於執行緒的阻止,考慮一下三個方面,不考慮IO阻塞的情況:

睡眠,等待,阻塞(因為需要一個物件的鎖定而被阻塞)。

1、睡眠

Thread.sleep(long millis)和Thread.sleep(long millis, int nanos)是靜態方法,他們強制將當前正在執行的執行緒休眠(暫停執行),以“減慢執行緒”。當執行緒睡眠時,它入睡在某個地方,在甦醒之前不會返回到可執行狀態。當睡眠時間到期,則返回到可執行狀態。

執行緒睡眠的原因:執行緒執行太快,或者需要強制進入下一輪,因為Java規範不保證合理的輪換。

睡眠的實現:呼叫靜態方法。

        try {             

                Thread.sleep(123);        

             } catch (InterruptedException e) {             

                 e.printStackTrace();         

           }

睡眠的位置:為了讓其他執行緒有機會執行,可以將Thread.sleep()的呼叫放執行緒run()之內。這樣才能保證該執行緒執行過程中會睡眠。

例如,在前面的例子中,將一個耗時的操作改為睡眠,以減慢執行緒的執行。可以這麼寫:

    public void run() {
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println(name + ": " + i);
        }

    }

執行結果:

yunhe: 0
tianti: 0
yunhe: 1
tianti: 1
yunhe: 2
tianti: 2
yunhe: 3
tianti: 3
yunhe: 4
tianti: 4

這樣,執行緒在每次執行過程中,總會睡眠10毫秒,睡眠後,其他的執行緒就有機會執行了。
注意:

  • 執行緒睡眠是幫助所有執行緒獲得執行機會的最好方法。
  • 執行緒睡眠到期自動甦醒,並返回到可執行狀態,不是執行狀態。sleep()中指定的時間是執行緒不會執行的最短時間。因此,sleep()方法不能保證該執行緒睡眠到期後就開始執行。
  • sleep()是靜態方法,只能控制當前正在執行的執行緒。

2、執行緒的優先順序和執行緒讓步yield()

執行緒的讓步是透過Thread.yield()來實現的。yield()方法的作用是:暫停當前正在執行的執行緒物件,並執行其他執行緒。

要理解yield(),必須瞭解執行緒的優先順序的概念。執行緒總是存在優先順序,優先順序範圍在1~10之間。JVM執行緒排程程式是基於優先順序的搶先排程機制。在大多數情況下,當前執行的執行緒優先順序將大於或等於執行緒池中任何執行緒的優先順序。但這僅僅是大多數情況。

注意:當設計多執行緒應用程式的時候,一定不要依賴於執行緒的優先順序。因為執行緒排程優先順序操作是沒有保障的,只能把執行緒優先順序作用作為一種提高程式效率的方法,但是要保證程式不依賴這種操作

當執行緒池中執行緒都具有相同的優先順序,排程程式的JVM實現自由選擇它喜歡的執行緒。這時候排程程式的操作有兩種可能:一是選擇一個執行緒執行,直到它阻塞或者執行完成為止。二是時間分片,為池內的每個執行緒提供均等的執行機會。

設定執行緒的優先順序:執行緒預設的優先順序是建立它的執行執行緒的優先順序。可以透過setPriority(int newPriority)更改執行緒的優先順序。

例如:

Thread t = new MyThread();
        t.setPriority(8);
        t.start();

 

執行緒優先順序為1~10之間的正整數,JVM從不會改變一個執行緒的優先順序。然而,1~10之間的值是沒有保證的。一些JVM可能不能識別10個不同的值,而將這些優先順序進行每兩個或多個合併,變成少於10個的優先順序,則兩個或多個優先順序的執行緒可能被對映為一個優先順序。 執行緒預設優先順序是5,Thread類中有三個常量,定義執行緒優先順序範圍:

static int MAX_PRIORITY           執行緒可以具有的最高優先順序。   10

static int MIN_PRIORITY           執行緒可以具有的最低優先順序。    1

static int NORM_PRIORITY           分配給執行緒的預設優先順序。   5

3、Thread.yield()方法

Thread.yield()方法作用是:暫停當前正在執行的執行緒物件,並執行其他執行緒。

yield()應該做的是讓當前執行執行緒回到可執行狀態,以允許具有相同優先順序的其他執行緒獲得執行機會。因此,使用yield()的目的是讓相同優先順序的執行緒之間能適當的輪轉執行。但是,實際中無法保證yield()達到讓步目的,因為讓步的執行緒還有可能被執行緒排程程式再次選中。
結論:yield()從未導致執行緒轉到等待/睡眠/阻塞狀態。在大多數情況下,yield()將導致執行緒從執行狀態轉到可執行狀態,但有可能沒有效果。

4、join()方法

Thread的非靜態方法join()讓一個執行緒B“加入”到另外一個執行緒A的尾部。在B執行完畢之前,A不能工作。例如:

        Thread t = new MyThread();         t.start();         t.join();

另外,join()方法還有帶超時限制的過載版本。 例如t.join(5000);則讓執行緒等待5000毫秒,如果超過這個時間,則停止等待,變為可執行狀態。

執行緒的加入join()對執行緒棧導致的結果是執行緒棧發生了變化,當然這些變化都是瞬時的。下面給示意圖:

 

小結:

到目前位置,介紹了執行緒離開執行狀態的3種方法:

  1. 呼叫Thread.sleep():使當前執行緒睡眠至少多少毫秒(儘管它可能在指定的時間之前被中斷)。
  2. 呼叫Thread.yield():不能保障太多事情,儘管通常它會讓當前執行執行緒回到可執行性狀態,使得有相同優先順序的執行緒有機會執行。
  3. 呼叫join()方法:保證當前執行緒停止執行,直到該執行緒所加入的執行緒完成為止。然而,如果它加入的執行緒沒有存活,則當前執行緒不需要停止。

 

除了以上三種方式外,還有下面幾種特殊情況可能使執行緒離開執行狀態:

1、執行緒的run()方法完成。

2、在物件上呼叫wait()方法(不是線上程上呼叫)。

3、執行緒不能在物件上獲得鎖定,它正試圖執行該物件的方法程式碼。

4、執行緒排程程式可以決定將當前執行狀態移動到可執行狀態,以便讓另一個執行緒獲得執行機會,而不需要任何理由。

 三、執行緒的同步和鎖

a、同步問題提出

執行緒的同步是為了防止多個執行緒訪問一個資料物件時,對資料造成的破壞。

b 、同步和鎖定

1、鎖的原理

Java中每個物件都有一個內建鎖當程式執行到非靜態的synchronized同步方法上時,自動獲得與正在執行程式碼類的當前例項(this例項)有關的鎖。獲得一個物件的鎖也稱為獲取鎖、鎖定物件、在物件上鎖定或在物件上同步。
 當程式執行到synchronized同步方法或程式碼塊時才該物件鎖才起作用。
 一個物件只有一個鎖。所以,如果一個執行緒獲得該鎖,就沒有其他執行緒可以獲得鎖,直到第一個執行緒釋放(或返回)鎖。這也意味著任何其他執行緒都不能進入該物件上的synchronized方法或程式碼塊,直到該鎖被釋放。
 
釋放鎖是指持鎖執行緒退出了synchronized同步方法或程式碼塊。
 
關於鎖和同步,有一下幾個要點:
1) 只能同步方法,而不能同步變數和類;
2) 每個物件只有一個鎖;當提到同步時,應該清楚在什麼上同步?也就是說,在哪個物件上同步?
3) 不必同步類中所有的方法,類可以同時擁有同步和非同步方法。
4) 如果兩個執行緒要執行一個類中的synchronized方法,並且兩個執行緒使用相同的例項來呼叫方法,那麼一次只能有一個執行緒能夠執行方法,另一個需要等待,直到鎖被釋放。也就是說:如果一個執行緒在物件上獲得一個鎖,就沒有任何其他執行緒可以進入(該物件的)類中的任何一個同步方法。
5) 如果執行緒擁有同步和非同步方法,則非同步方法可以被多個執行緒自由訪問而不受鎖的限制。
6) 執行緒睡眠時,它所持的任何鎖都不會釋放。
7) 執行緒可以獲得多個鎖。比如,在一個物件的同步方法裡面呼叫另外一個物件的同步方法,則獲取了兩個物件的同步鎖。
8) 同步損害併發性,應該儘可能縮小同步範圍。同步不但可以同步整個方法,還可以同步方法中一部分程式碼塊。
9) 在使用同步程式碼塊時候,應該指定在哪個物件上同步,也就是說要獲取哪個物件的鎖。例如:
    public int fix(int y) {
        synchronized (this) {
            x = x - y;
        }
        return x;
    }
當然,同步方法也可以改寫為非同步方法,但功能完全一樣的,例如:
 
   public synchronized int getX() {
        return x++;
    }
    public int getX() {
        synchronized (this) {
            return x;
        }
    }
效果是完全一樣的。

c、靜態方法同步

要同步靜態方法,需要一個用於整個類物件的鎖,這個物件是就是這個類(XXX.class)。
例如:
public static synchronized int setName(String name){
      Xxx.name = name;
}
等價於
public static int setName(String name){
      synchronized(Xxx.class){
            Xxx.name = name;
      }
}

d、如果執行緒不能不能獲得鎖會怎麼樣

如果執行緒試圖進入同步方法,而其鎖已經被佔用,則執行緒在該物件上被阻塞。實質上,執行緒進入該物件的的一種池中,必須在哪裡等待,直到其鎖被釋放,該執行緒再次變為可執行或執行為止。
 
當考慮阻塞時,一定要注意哪個物件正被用於鎖定:
1、呼叫同一個物件中非靜態同步方法的執行緒將彼此阻塞。如果是不同物件,則每個執行緒有自己的物件的鎖,執行緒間彼此互不干預。
2、呼叫同一個類中的靜態同步方法的執行緒將彼此阻塞,它們都是鎖定在相同的Class物件上。
3、靜態同步方法和非靜態同步方法將永遠不會彼此阻塞,因為靜態方法鎖定在Class物件上,非靜態方法鎖定在該類的物件上。
4、對於同步程式碼塊,要看清楚什麼物件已經用於鎖定(synchronized後面括號的內容)。在同一個物件上進行同步的執行緒將彼此阻塞,在不同物件上鎖定的執行緒將永遠不會彼此阻塞。

e、何時需要同步

在多個執行緒同時訪問互斥(可交換)資料時,應該同步以保護資料,確保兩個執行緒不會同時修改更改它。
 對於非靜態欄位中可更改的資料,通常使用非靜態方法訪問。
對於靜態欄位中可更改的資料,通常使用靜態方法訪問。

f、執行緒安全類

當一個類已經很好的同步以保護它的資料時,這個類就稱為“執行緒安全的”。
即使是執行緒安全類,也應該特別小心,因為操作的執行緒是間仍然不一定安全。
 
舉個形象的例子,比如一個集合是執行緒安全的,有兩個執行緒在操作同一個集合物件,當第一個執行緒查詢集合非空後,刪除集合中所有元素的時候。第二個執行緒也來執行與第一個執行緒相同的操作,也許在第一個執行緒查詢後,第二個執行緒也查詢出集合非空,但是當第一個執行清除後,第二個再執行刪除顯然是不對的,因為此時集合已經為空了。
看個程式碼:
public class NameList { 
    private List nameList = Collections.synchronizedList(new LinkedList()); 

    public void add(String name) { 
        nameList.add(name); 
    } 

    public String removeFirst() { 
        if (nameList.size() > 0) { 
            return (String) nameList.remove(0); 
        } else { 
            return null; 
        } 
    } 
}

測試程式碼:

public class Test { 
    public static void main(String[] args) { 
        final NameList nl = new NameList(); 
        nl.add("aaa"); 
        class NameDropper extends Thread{ 
            public void run(){ 
                String name = nl.removeFirst(); 
                System.out.println(name); 
            } 
        } 

        Thread t1 = new NameDropper(); 
        Thread t2 = new NameDropper(); 
        t1.start(); 
        t2.start(); 
    } 
}
雖然集合物件
    private List nameList = Collections.synchronizedList(new LinkedList());
是同步的,但是程式還不是執行緒安全的。
出現這種事件的原因是,上例中一個執行緒操作列表過程中無法阻止另外一個執行緒對列表的其他操作。
 
解決上面問題的辦法是,在操作集合物件的NameList上面做一個同步。改寫後的程式碼如下:
public class NameList { 
    private List nameList = Collections.synchronizedList(new LinkedList()); 

    public synchronized void add(String name) { 
        nameList.add(name); 
    } 

    public synchronized String removeFirst() { 
        if (nameList.size() > 0) { 
            return (String) nameList.remove(0); 
        } else { 
            return null; 
        } 
    } 
}

這樣,當一個執行緒訪問其中一個同步方法時,其他執行緒只有等待。

g、執行緒死鎖
 
死鎖對Java程式來說,是很複雜的,也很難發現問題。當兩個執行緒被阻塞,每個執行緒在等待另一個執行緒時就發生死鎖。
 
還是看一個比較直觀的死鎖例子
public class DeadlockRisk { 
    private static class Resource { 
        public int value; 
    } 

    private Resource resourceA = new Resource(); 
    private Resource resourceB = new Resource(); 

    public int read() { 
        synchronized (resourceA) { 
            synchronized (resourceB) { 
                return resourceB.value + resourceA.value; 
            } 
        } 
    } 

    public void write(int a, int b) { 
        synchronized (resourceB) { 
            synchronized (resourceA) { 
                resourceA.value = a; 
                resourceB.value = b; 
            } 
        } 
    } 
}
假設read()方法由一個執行緒啟動,write()方法由另外一個執行緒啟動。讀執行緒將擁有resourceA鎖,寫執行緒將擁有resourceB鎖,兩者都堅持等待的話就出現死鎖。
 實際上,上面這個例子發生死鎖的機率很小。因為在程式碼內的某個點,CPU必須從讀執行緒切換到寫執行緒,所以,死鎖基本上不能發生。
 但是,無論程式碼中發生死鎖的機率有多小,一旦發生死鎖,程式就死掉。有一些設計方法能幫助避免死鎖,包括始終按照預定義的順序獲取鎖這一策略。

h、執行緒同步小結

1、執行緒同步的目的是為了保護多個執行緒反問一個資源時對資源的破壞。
2、執行緒同步方法是透過鎖來實現,每個物件都有切僅有一個鎖,這個鎖與一個特定的物件關聯,執行緒一旦獲取了物件鎖,其他訪問該物件的執行緒就無法再訪問該物件的其他同步方法。
3、對於靜態同步方法,鎖是針對這個類的,鎖物件是該類的Class物件。靜態和非靜態方法的鎖互不干預。一個執行緒獲得鎖,當在一個同步方法中訪問另外物件上的同步方法時,會獲取這兩個物件鎖。
4、對於同步,要時刻清醒在哪個物件上同步,這是關鍵。
5、編寫執行緒安全的類,需要時刻注意對多個執行緒競爭訪問資源的邏輯和安全做出正確的判斷,對“原子”操作做出分析,並保證原子操作期間別的執行緒無法訪問競爭資源。
6、當多個執行緒等待一個物件鎖時,沒有獲取到鎖的執行緒將發生阻塞。
7、死鎖是執行緒間相互等待鎖造成的,在實際中發生的機率非常的小。真讓你寫個死鎖程式,不一定好使,呵呵。但是,一旦程式發生死鎖,程式將死掉。

 

相關文章