Java併發編程-問題(三)

原文:http://blog.sina.com.cn/s/blog_4b6047bc010009co.html

    線程之間共享數據引起了併發執行程序中的同步問題。那些數據是可能需要同步訪問的呢?很簡單,線程之間能夠共享的數據,也就是對多個線程可見的數據。
    Java的數據有兩種基本類型內存分配模式(不算虛擬機內部類型,詳細內容參見虛擬機規範):運行時棧和堆兩種。由於運行時棧是線程所私有的,它主要用來保存局部變量和中間運算結
果,因此它們的數據是不可能被線程之間所共享的。內存堆是創建類對象和數組地方,它們是被虛擬機內各個線程所共享的,因此如果一個線程能獲得某個堆對象的引用,那麼就稱
這個對象是對該線程可見的。
    線程之間通信基本上通過共享對象引用來達到共享對象的簡單類型字段和引用字段。由於不涉及I/O操作,這種模式的共享比IPC共享要高效的多。但也使得兩類錯誤成爲可能:線程干擾
和內存一致性錯誤。防止此類問題發生的線程編程技術稱作同步。我們詳述一下這兩個錯誤的概念。
線程干擾
    考察下面的一段代碼:
class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }

}
    Counter的目的是對increment方法的調用將c增加1,對decrement的調用將c減去1。然而如果一個Counter對象被多個線程所引用,那麼這些線程之間的干擾就讓我們經
常得不到期望的結果。
    當運行在不同線程中對同一對象進行訪問的兩個操作發生時,干擾就會產生。這是因爲這兩個操作往往是由多步組成的,而且它們的執行順序是可以互相交織的。
    表面看來,Counter對象的increment和decrement操作是不可能交織的,每個操作都是一個簡單的Java語句。實際上這些語句都已經被虛擬機翻譯成了好幾步的指令。我們不再詳
細描述虛擬機所採用的指令步驟,只需知道一個c++操作可能被虛擬機分爲三步:
   1.獲取c的當前值
   2.將該值加上1
   3.將加的結果保存回變量c
    c--也是同樣三步,除了第二步進行減操作外。
    假設A線程調用increment方法的同時B線程調用decrement方法,而c的初始值爲0,那麼它們的交織的指令動作可能依着下面的順序進行:
   1. 線程A: 獲取c.
   2. 線程B: 獲取c.
   3. 線程A: 獲取的值加1,結果1
   4. 線程B: 獲取的值減1,結果-1
   5. 線程A: 將1保存到c變量中,c結果是1
   6. 線程B: 將-1保存到c變量中,c結果是-1

    線程A的結果丟失了,被線程B的覆蓋了。這種特殊的交織順序只產生一種結果。但在另一種情況下,也可能B的結果被覆蓋,或者乾脆沒有錯誤。由於它們執行順序的不確定性,線程干擾的錯誤將很難定位和修改。
內存一致性錯誤
    當不同線程對同一個數據看到不同視圖時,內存一致性錯誤就發生了。內存一致性錯誤發生的原因很複雜,不是一兩句話能解釋得清楚的,在這兒就不再詳述。我們只需知道防備這種錯誤發生的策略就行
了。
    避免內存一致性錯誤的關鍵是理解“發生過”(happens-before)關係,這個關係是保證被某語句寫過內存的結果對其他某個語句是可見的。爲理解這一點,考慮下面的例子,假設有個
簡單的int字段這樣定義:
int counter=0;
    這個counter字段在兩個線程A和B之間共享。假設線程A增加counter:
counter++;
    緊接着,線程B打印出counter:
System.out.println(counter);
    如果這兩句話在同一個線程內執行,那麼假設打印結果爲1是安全的。但如果當這兩個語句是在不同線程中執行的,那麼打印出的值完全有可能是0。因爲線程A對於
counter的改變不一定能對線程B可見,除非程序在這兩條語句之間建立了“發生過”關係。
    有幾種動作能建立“發生過”關係,目前我們已經接觸的能產生“發生過”關係的動作包括:
* 當語句調用Thread.start方法,任何和Thread.start建立有“發生過”關係的語句和新啓動線程執行的每條語句都有“發生過”關係。創建新線程代碼的效果對於這個新
線程是可見的。
* 當線程結束並導致另一個線程的Thread.join返回,那麼結束線程所執行的所有語句和join後面的語句都有“發生過”關係。

    當然建立這種“發生過”關係的方法除了上面兩種之外,還有以後文章將詳細講述的互斥-同步技術。

    目前我們描述了兩種因爲數據共享而發生的問題:線程干擾和內存一致性錯誤。線程干擾經常在“寫”和“寫”操作之間,當着兩個動作需要產生疊加的效果時。而內存一致性錯誤經常發生在“寫”和“讀”之間,當讀發生在寫之後需要看到寫的效果時。這兩種錯誤是互斥-併發技術要解決的兩大類問題。這兩大類問題分別對應前文所講的互斥和同步問題。其中避免線程干擾往往不需要保證操作的順序,只要保證兩個操作互斥進行就行了。而避免內存一致性錯誤除了需要操作要互斥以外,還需要規定動作的發生順序,即需要同步

    後續的文章將介紹在Java中如何解決這兩種因內存共享引起的問題技術。

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