[轉]Java線程安全總結

      最近想將java基礎的一些東西都整理整理,寫下來,這是對知識的總結,也是一種樂趣。已經擬好了提綱,大概分爲這幾個主題: java線程安全,java垃圾收集,java併發包詳細介紹,java profile和jvm性能調優慢 慢寫吧。本人jameswxx原創文章,轉載請註明出處,我費了不少心血,多謝了。關於java線程安全,網上有不少資料,我只想從本身的角度總結對這方 面的考慮,有時候寫東西是很痛苦的,知道一些東西,但想用文字說清楚,卻不是那麼容易。我認爲要認識java線程安全,必須瞭解兩個主要的點:java的 內存模型,java的線程同步機制。特別是內存模型,java的線程同步機制很大程度上都是基於內存模型而設定的。後面我還會寫java併發包的文章,詳 細總結如何利用java併發包編寫高效安全的多線程併發程序。暫時寫得比較倉促,後面會慢慢補充完善。javascript

 

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

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. }   

 

爲了保證銀行帳戶的安全,能夠操做帳戶的方法以下: app

Java代碼   收藏代碼
  1. public synchronized void add(int num) {  
  2.      balance = balance + num;  
  3. }  
  4. public synchronized void withdraw(int num) {  
  5.      balance = balance - num;  
  6. }  

 

剛纔不是說了synchronized的用法是這樣的嗎: jvm

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

 

那麼對於public synchronized void add(int num)這種狀況,意味着什麼呢?其實這種狀況,鎖就是這個方法所在的對象。同理,若是方法是public  static synchronized void add(int num),那麼鎖就是這個方法所在的class。         理論上,每一個對象均可以作爲鎖,但一個對象作爲鎖時,應該被多個線程共享,這樣才顯得有意義,在併發環境下,一個沒有共享的對象做爲鎖是沒有意義的。假若有這樣的代碼: 性能

Java代碼   收藏代碼
  1. public class ThreadTest{  
  2.   public void test(){  
  3.      Object lock=new Object();  
  4.      synchronized (lock){  
  5.         //do something  
  6.      }  
  7.   }  
  8. }  

 

lock變量做爲一個鎖存在根本沒有意義,由於它根本不是共享對象,每一個線程進來都會執行Object lock=new Object();每一個線程都有本身的lock,根本不存在鎖競爭。         每一個鎖對象都有兩個隊列,一個是就緒隊列,一個是阻塞隊列,就緒隊列存儲了將要得到鎖的線程,阻塞隊列存儲了被阻塞的線程,當一個被線程被喚醒 (notify)後,纔會進入到就緒隊列,等待cpu的調度。當一開始線程a第一次執行account.add方法時,jvm會檢查鎖對象account 的就緒隊列是否已經有線程在等待,若是有則代表account的鎖已經被佔用了,因爲是第一次運行,account的就緒隊列爲空,因此線程a得到了鎖, 執行account.add方法。若是剛好在這個時候,線程b要執行account.withdraw方法,由於線程a已經得到了鎖尚未釋放,因此線程 b要進入account的就緒隊列,等到獲得鎖後才能夠執行。 一個線程執行臨界區代碼過程以下: 1 得到同步鎖 2 清空工做內存 3 從主存拷貝變量副本到工做內存 4 對這些變量計算 5 將變量從工做內存寫回到主存 6 釋放鎖 可見,synchronized既保證了多線程的併發有序性,又保證了多線程的內存可見性。
生產者/消費者模式         生產者/消費者模式實際上是一種很經典的線程同步模型,不少時候,並非光保證多個線程對某共享資源操做的互斥性就夠了,每每多個線程之間都是有協做的。         假設有這樣一種狀況,有一個桌子,桌子上面有一個盤子,盤子裏只能放一顆雞蛋,A專門往盤子裏放雞蛋,若是盤子裏有雞蛋,則一直等到盤子裏沒雞蛋,B專門 從盤子裏拿雞蛋,若是盤子裏沒雞蛋,則等待直到盤子裏有雞蛋。其實盤子就是一個互斥區,每次往盤子放雞蛋應該都是互斥的,A的等待其實就是主動放棄鎖,B 等待時還要提醒A放雞蛋。 如何讓線程主動釋放鎖 很簡單,調用鎖的wait()方法就好。wait方法是從Object來的,因此任意對象都有這個方法。看這個代碼片斷:
this

Java代碼   收藏代碼
  1. Object lock=new Object();//聲明瞭一個對象做爲鎖  
  2.    synchronized (lock) {  
  3.        balance = balance - num;  
  4.        //這裏放棄了同步鎖,好不容易獲得,又放棄了  
  5.        lock.wait();  
  6. }  

 

若是一個線程得到了鎖lock,進入了同步塊,執行lock.wait(),那麼這個線程會進入到lock的阻塞隊列。若是調用lock.notify()則會通知阻塞隊列的某個線程進入就緒隊列。 聲明一個盤子,只能放一個雞蛋

Java代碼   收藏代碼
  1. package com.jameswxx.synctest;  
  2. public class Plate{  
  3.   List<Object> eggs=new ArrayList<Object>();  
  4.   public synchronized  Object getEgg(){  
  5.      if(eggs.size()==0){  
  6.         try{  
  7.             wait();  
  8.         }catch(InterruptedException e){  
  9.         }  
  10.      }  
  11.   
  12.     Object egg=eggs.get(0);  
  13.     eggs.clear();//清空盤子  
  14.     notify();//喚醒阻塞隊列的某線程到就緒隊列  
  15.     return egg;  
  16. }  
  17.   
  18.  public synchronized  void putEgg(Object egg){  
  19.     If(eggs.size()>0){  
  20.       try{  
  21.          wait();  
  22.       }catch(InterruptedException e){  
  23.       }  
  24.     }  
  25.     eggs.add(egg);//往盤子裏放雞蛋  
  26.     notify();//喚醒阻塞隊列的某線程到就緒隊列  
  27.   }  
  28. }  

 

聲明一個Plate對象爲plate,被線程A和線程B共享,A專門放雞蛋,B專門拿雞蛋。假設 1 開始,A調用plate.putEgg方法,此時eggs.size()爲0,所以順利將雞蛋放到盤子,還執行了notify()方法,喚醒鎖的阻塞隊列的線程,此時阻塞隊列尚未線程。 2 又有一個A線程對象調用plate.putEgg方法,此時eggs.size()不爲0,調用wait()方法,本身進入了鎖對象的阻塞隊列。 3 此時,來了一個B線程對象,調用plate.getEgg方法,eggs.size()不爲0,順利的拿到了一個雞蛋,還執行了notify()方法,喚 醒鎖的阻塞隊列的線程,此時阻塞隊列有一個A線程對象,喚醒後,它進入到就緒隊列,就緒隊列也就它一個,所以立刻獲得鎖,開始往盤子裏放雞蛋,此時盤子是 空的,所以放雞蛋成功。 4 假設接着來了線程A,就重複2;假設來料線程B,就重複3。 整個過程都保證了放雞蛋,拿雞蛋,放雞蛋,拿雞蛋。

 

volatile關鍵字        volatile是java提供的一種同步手段,只不過它是輕量級的同步,爲何這麼說,由於volatile只能保證多線程的內存可見性,不能保證多線 程的執行有序性。而最完全的同步要保證有序性和可見性,例如synchronized。任何被volatile修飾的變量,都不拷貝副本到工做內存,任何 修改都及時寫在主存。所以對於Valatile修飾的變量的修改,全部線程立刻就能看到,可是volatile不能保證對變量的修改是有序的。什麼意思 呢?假若有這樣的代碼:

Java代碼   收藏代碼
  1. public class VolatileTest{  
  2.   public volatile int a;  
  3.   public void add(int count){  
  4.        a=a+count;  
  5.   }  
  6. }  

 

        當一個VolatileTest對象被多個線程共享,a的值不必定是正確的,由於a=a+count包含了好幾步操做,而此時多個線程的執行是無序的,因 爲沒有任何機制來保證多個線程的執行有序性和原子性。volatile存在的意義是,任何線程對a的修改,都會立刻被其餘線程讀取到,由於直接操做主存, 沒有線程對工做內存和主存的同步。因此,volatile的使用場景是有限的,在有限的一些情形下可使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全,必須同時知足下面兩個條件: 1)對變量的寫操做不依賴於當前值。 2)該變量沒有包含在具備其餘變量的不變式中 volatile只保證了可見性,因此Volatile適合直接賦值的場景,如

Java代碼   收藏代碼
  1. public class VolatileTest{  
  2.   public volatile int a;  
  3.   public void setA(int a){  
  4.       this.a=a;  
  5.   }  
  6. }  

 

在沒有volatile聲明時,多線程環境下,a的最終值不必定是正確 的,由於this.a=a;涉及到給a賦值和將a同步回主存的步驟,這個順序可能被打亂。若是用volatile聲明瞭,讀取主存副本到工做內存和同步a 到主存的步驟,至關因而一個原子操做。因此簡單來講,volatile適合這種場景:一個變量被多個線程共享,線程直接給這個變量賦值。這是一種很簡單的 同步場景,這時候使用volatile的開銷將會很是小。

相關文章
相關標籤/搜索