1. 程式人生 > >對Java執行緒安全與不安全的理解

對Java執行緒安全與不安全的理解

  當我們檢視JDK API的時候,總會發現一些類說明寫著,執行緒安全或者執行緒不安全,比如說到StringBuilder中,有這麼一句,“將StringBuilder 的例項用於多個執行緒是不安全的。如果需要這樣的同步,則建議使用StringBuffer。”,提到StringBuffer時,說到“StringBuffer是執行緒安全的可變字元序列,一個類似於String的字串緩衝區,雖然在任意時間點上它都包含某種特定的字元序列,但通過某些方法呼叫可以改變該序列的長度和內容。可將字串緩衝區安全地用於多個執行緒。可以在必要時對這些方法進行同步,因此任意特定例項上的所有操作就好像是以序列順序發生的,該順序與所涉及的每個執行緒進行的方法呼叫順序一致

”。StringBuilder是一個可變的字元序列,此類提供一個與StringBuffe相容的API,但不保證同步。該類被設計用作StringBuffer的一個簡易替換,用在字串緩衝區被單個執行緒使用的時候(這種情況很普遍)。如果可能,建議優先採用該類,因為在大多數實現中,它比StringBuffer要快。將StringBuilder的例項用於多個執行緒是不安全的,如果需要這樣的同步,則建議使用StringBuffer。

   根據以上JDK文件中對StringBuffer和StringBuilder的描述,得到對String、StringBuilder與StringBuffer三者使用情況的總結:
   1、如果要操作少量的資料用String
   2、單執行緒操作字串緩衝區下操作大量資料StringBuilder
   3、多執行緒操作字串緩衝區下操作大量資料StringBuffer

   那麼下面手動建立一個執行緒不安全的類,然後在多執行緒中使用這個類,看看有什麼效果。

public class Count {  
    private int num;  
    public void count() {  
        for(int i = 1; i <= 10; i++) {  
            num += i;  
        }  
        System.out.println(Thread.currentThread().getName() + "-" + num);  
    }  
}  

   在這個類中的count方法計算1一直加到10的和,並輸出當前執行緒名和總和,我們期望的是每個執行緒都會輸出55。

public class ThreadTest {  
    public static void main(String[] args) {  
        Runnable runnable = new Runnable() {  
            Count count = new Count();  
            public void run() {  
                count.count();  
            }  
        };  

        for(int i = 0; i < 10; i++) {  
            new Thread(runnable).start();  
        }  
    }  
}  

   這裡啟動了10個執行緒,看一下輸出結果:

Thread-0-55  
Thread-1-110  
Thread-2-165  
Thread-4-220  
Thread-5-275  
Thread-6-330  
Thread-3-385  
Thread-7-440  
Thread-8-495  
Thread-9-550  

   只有Thread-0執行緒輸出的結果是我們期望的,而輸出的是每次都累加的,要想得到我們期望的結果,有幾種解決方案:

   1、將Count類中的成員變數num變成count方法的區域性變數;

public class Count {  
    public void count() {  
        int num = 0;  
        for(int i = 1; i <= 10; i++) {  
            num += i;  
        }  
        System.out.println(Thread.currentThread().getName() + ”-“ + num);  
    }  
}  

   2、將執行緒類成員變數拿到run方法中,這時count引用是執行緒內的區域性變數;

public class ThreadTest4 {  
    public static void main(String[] args) {  
        Runnable runnable = new Runnable() {  
            public void run() {  
                Count count = new Count();  
                count.count();  
            }  
        };  
        for(int i = 0; i < 10; i++) {  
            new Thread(runnable).start();  
        }  
    }  
}   

   3、每次啟動一個執行緒使用不同的執行緒類,不推薦。

   通過上述測試,我們發現,存在成員變數的類用於多執行緒時是不安全的,不安全體現在這個成員變數可能發生非原子性的操作,而變數定義在方法內也就是區域性變數是執行緒安全的。想想在使用struts1時,不推薦建立成員變數,因為action是單例的,如果建立了成員變數,就會存線上程不安全的隱患,而struts2是每一次請求都會建立一個action,就不用考慮執行緒安全的問題。所以,日常開發中,通常需要考慮成員變數或者說全域性變數在多執行緒環境下,是否會引發一些問題

   要說明執行緒同步問題首先要說明Java執行緒的兩個特性,可見性和有序性

   多個執行緒之間是不能直接傳遞資料進行互動的,它們之間的互動只能通過共享變數來實現。拿上面的例子來說明,在多個執行緒之間共享了Count類的一個例項,這個物件是被建立在主記憶體(堆記憶體)中,每個執行緒都有自己的工作記憶體(執行緒棧),工作記憶體儲存了主記憶體count物件的一個副本,當執行緒操作count物件時,首先從主記憶體複製count物件到工作記憶體中,然後執行程式碼count.count(),改變了num值,最後用工作記憶體中的count重新整理主記憶體的 count。當一個物件在多個工作記憶體中都存在副本時,如果一個工作記憶體重新整理了主記憶體中的共享變數,其它執行緒也應該能夠看到被修改後的值,此為可見性

   多個執行緒執行時,CPU對執行緒的排程是隨機的,我們不知道當前程式被執行到哪步就切換到了下一個執行緒,一個最經典的例子就是銀行匯款問題,一個銀行賬戶存款100,這時一個人從該賬戶取10元,同時另一個人向該賬戶匯10元,那麼餘額應該還是100。那麼此時可能發生這種情況,A執行緒負責取款,B執行緒負責匯款,A從主記憶體讀到100,B從主記憶體讀到100,A執行減10操作,並將資料重新整理到主記憶體,這時主記憶體資料100-10=90,而B記憶體執行加10操作,並將資料重新整理到主記憶體,最後主記憶體資料100+10=110,顯然這是一個嚴重的問題,我們要保證A執行緒和B執行緒有序執行,先取款後匯款或者先匯款後取款,此為有序性。