Java線程安全總結

關鍵字: java , 線程安全 , synchronized , volatile

轉自:http://jameswxx.iteye.com/blog/806968

本文主要內容:
1.什麼是可見性
2.什麼是有序性
3.多線程情況下如何保證可見性和有序性
4.synchronized關鍵字和volatile關鍵字介紹

 

淺談java內存模型 
       不同的平臺,內存模型是不一樣的,但是jvm的內存模型規範是統一的。其實java的多線程併發問題最終都會反映在java的內存模型上,所謂線程安全無非是要控制多個線程對某個資源的有序訪問或修改。總結java的內存模型,要解決兩個主要的問題:可見性和有序性。我們都知道計算機有高速緩存的存在,處理器並不是每次處理數據都是取內存的。JVM定義了自己的內存模型,屏蔽了底層平臺內存管理細節,對於java開發人員,要清楚在jvm內存模型的基礎上,如果解決多線程的可見性和有序性。
       那麼,何謂可見性? 多個線程之間是不能互相傳遞數據通信的,它們之間的溝通只能通過共享變量來進行。Java內存模型(JMM)規定了jvm有主內存,主內存是多個線程共享的。當new一個對象的時候,也是被分配在主內存中,每個線程都有自己的工作內存,工作內存存儲了主存的某些對象的副本,當然線程的工作內存大小是有限制的。當線程操作某個對象時,執行順序如下:
 (1) 從主存複製變量到當前工作內存 (read and load)
 (2) 執行代碼,改變共享變量值 (use and assign)
 (3) 用工作內存數據刷新主存相關內容 (store and write)
 

JVM規範定義了線程對主存的操作指令:read,load,use,assign,store,write。當一個共享變量在多個線程的工作內存中都有副本時,如果一個線程修改了這個共享變量,那麼其他線程應該能夠看到這個被修改後的值,這就是多線程的可見性問題。
        那麼,什麼是有序性呢 ?線程在引用變量時不能直接從主內存中引用,如果線程工作內存中沒有該變量,則會從主內存中拷貝一個副本到工作內存中,這個過程爲read-load,完成後線程會引用該副本。當同一線程再度引用該字段時,有可能重新從主存中獲取變量副本(read-load-use),也有可能直接引用原來的副本(use),也就是說 read,load,use順序可以由JVM實現系統決定。
        線程不能直接爲主存中中字段賦值,它會將值指定給工作內存中的變量副本(assign),完成後這個變量副本會同步到主存儲區(store-write),至於何時同步過去,根據JVM實現系統決定.有該字段,則會從主內存中將該字段賦值到工作內存中,這個過程爲read-load,完成後線程會引用該變量副本,當同一線程多次重複對字段賦值時,比如:

Java代碼  
  1. for(int i=0;i<10;i++)  
  2.  a++;  

線程有可能只對工作內存中的副本進行賦值,只到最後一次賦值後才同步到主存儲區,所以assign,store,weite順序可以由JVM實現系統決定。假設有一個共享變量x,線程a執行x=x+1。從上面的描述中可以知道x=x+1並不是一個原子操作,它的執行過程如下:

1 從主存中讀取變量x副本到工作內存
2 給x加1
3 將x加1後的值寫回主
 存
如果另外一個線程b執行x=x-1,執行過程如下:
1 從主存中讀取變量x副本到工作內存
2 給x減1
3 將x減1後的值寫回主存
 
那麼顯然,最終的x的值是不可靠的。假設x現在爲10,線程a加1,線程b減1,從表面上看,似乎最終x還是爲10,但是多線程情況下會有這種情況發生:
1:線程a從主存讀取x副本到工作內存,工作內存中x值爲10
2:線程b從主存讀取x副本到工作內存,工作內存中x值爲10
3:線程a將工作內存中x加1,工作內存中x值爲11
4:線程a將x提交主存中,主存中x爲11
5:線程b將工作內存中x值減1,工作內存中x值爲9
6:線程b將x提交到中主存中,主存中x爲9
 
同樣,x有可能爲11,如果x是一個銀行賬戶,線程a存款,線程b扣款,顯然這樣是有嚴重問題的,要解決這個問題,必須保證線程a和線程b是有序執行的,並且每個線程執行的加1或減1是一個原子操作。看看下面代碼:

Java代碼  
  1. public class Account {  
  2.   
  3.     private int balance;  
  4.   
  5.     public Account(int balance) {  
  6.         this.balance = balance;  
  7.     }  
  8.   
  9.     public int getBalance() {  
  10.         return balance;  
  11.     }  
  12.   
  13.     public void add(int num) {  
  14.         balance = balance + num;  
  15.     }  
  16.   
  17.     public void withdraw(int num) {  
  18.         balance = balance - num;  
  19.     }  
  20.   
  21.     public static void main(String[] args) throws InterruptedException {  
  22.         Account account = new Account(1000);  
  23.         Thread a = new Thread(new AddThread(account, 20), "add");  
  24.         Thread b = new Thread(new WithdrawThread(account, 20), "withdraw");  
  25.         a.start();  
  26.         b.start();  
  27.         a.join();  
  28.         b.join();  
  29.         System.out.println(account.getBalance());  
  30.     }  
  31.   
  32.     static class AddThread implements Runnable {  
  33.         Account account;  
  34.         int     amount;  
  35.   
  36.         public AddThread(Account account, int amount) {  
  37.             this.account = account;  
  38.             this.amount = amount;  
  39.         }  
  40.   
  41.         public void run() {  
  42.             for (int i = 0; i < 200000; i++) {  
  43.                 account.add(amount);  
  44.             }  
  45.         }  
  46.     }  
  47.   
  48.     static class WithdrawThread implements Runnable {  
  49.         Account account;  
  50.         int     amount;  
  51.   
  52.         public WithdrawThread(Account account, int amount) {  
  53.             this.account = account;  
  54.             this.amount = amount;  
  55.         }  
  56.   
  57.         public void run() {  
  58.             for (int i = 0; i < 100000; i++) {  
  59.                 account.withdraw(amount);  
  60.             }  
  61.         }  
  62.     }  
  63. }  

第一次執行結果爲10200,第二次執行結果爲1060,每次執行的結果都是不確定的,因爲線程的執行順序是不可預見的。這是java同步產生的根源,synchronized關鍵字保證了多個線程對於同步塊是互斥的,synchronized作爲一種同步手段,解決java多線程的執行有序性和內存可見性,而volatile關鍵字之解決多線程的內存可見性問題。後面將會詳細介紹。


synchronized關鍵字 
        上面說了,java用synchronized關鍵字做爲多線程併發環境的執行有序性的保證手段之一。當一段代碼會修改共享變量,這一段代碼成爲互斥區或臨界區,爲了保證共享變量的正確性,synchronized標示了臨界區。典型的用法如下:

Java代碼  
  1. synchronized(鎖){  
  2.      臨界區代碼  
  3. }   

爲了保證銀行賬戶的安全,可以操作賬戶的方法如下:

Java代碼  
  1. public synchronized void add(int num) {  
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章