對Java線程安全與不安全的理解

參考文獻:

https://blog.csdn.net/fuzhongmin05/article/details/59110866

當我們查看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線程有序執行,先取款後匯款或者先匯款後取款,此爲有序性。
 

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