java線程安全總結(一)

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

淺談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++;  併發

 for(int i=0;i<10;i++)
  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是一個原子操做。看看下面代碼:
jvm

Java代碼          性能

  1. public class Account {  測試

  2.     private int balance;  this

 

  1.     public Account(int balance) {  

  2.         this.balance = balance;  

  3.     }  

  4.     public int getBalance() {  

  5.         return balance;  

  6.     }  

  7.     public void add(int num) {  

  8.         balance = balance + num;  

  9.     }  

 

  1.     public void withdraw(int num) {  

  2.         balance = balance - num;  

  3.     }  

 

  1.     public static void main(String[] args) throws InterruptedException {  

  2.         Account account = new Account(1000);  

  3.         Thread a = new Thread(new AddThread(account, 20), "add");  

  4.         Thread b = new Thread(new WithdrawThread(account, 20), "withdraw");  

  5.         a.start();  

  6.         b.start();  

  7.         a.join();  

  8.         b.join();  

  9.         System.out.println(account.getBalance());  

  10.     }  

  11.     static class AddThread implements Runnable {  

  12.         Account account;  

  13.         int     amount;  

  14.         public AddThread(Account account, int amount) {  

  15.             this.account = account;  

  16.             this.amount = amount;  

  17.         }  

  18.         public void run() {  

  19.             for (int i = 0; i < 200000; i++) {  

  20.                 account.add(amount);  

  21.             }  

  22.         }  

  23.     }  

  24.     static class WithdrawThread implements Runnable {  

  25.         Account account;  

  26.         int     amount;  

  27.         public WithdrawThread(Account account, int amount) {  

  28.             this.account = account;  

  29.             this.amount = amount;  

  30.         }  

  31.         public void run() {  

  32.             for (int i = 0; i < 100000; i++) {  

  33.                 account.withdraw(amount);  

  34.             }  

  35.         }  

  36.     }  

  37. }  

public class Account {

    private int balance;

    public Account(int balance) {
        this.balance = balance;
    }

    public int getBalance() {
        return balance;
    }

    public void add(int num) {
        balance = balance + num;
    }

    public void withdraw(int num) {
        balance = balance - num;
    }

    public static void main(String[] args) throws InterruptedException {
        Account account = new Account(1000);
        Thread a = new Thread(new AddThread(account, 20), "add");
        Thread b = new Thread(new WithdrawThread(account, 20), "withdraw");
        a.start();
        b.start();
        a.join();
        b.join();
        System.out.println(account.getBalance());
    }

    static class AddThread implements Runnable {
        Account account;
        int     amount;

        public AddThread(Account account, int amount) {
            this.account = account;
            this.amount = amount;
        }

        public void run() {
            for (int i = 0; i < 200000; i++) {
                account.add(amount);
            }
        }
    }

    static class WithdrawThread implements Runnable {
        Account account;
        int     amount;

        public WithdrawThread(Account account, int amount) {
            this.account = account;
            this.amount = amount;
        }

        public void run() {
            for (int i = 0; i < 100000; i++) {
                account.withdraw(amount);
            }
        }
    }
}

 


第一次執行結果爲10200,第二次執行結果爲1060,每次執行的結果都是不肯定的,由於線程的執行順序是不可預見的。這是java同步產生的根 源,synchronized關鍵字保證了多個線程對於同步塊是互斥的,synchronized做爲一種同步手段,解決java多線程的執行有序性和內 存可見性,而volatile關鍵字之解決多線程的內存可見性問題。後面將會詳細介紹。  
synchronized關鍵 字  
         上面說了,java用synchronized關鍵字作爲多線程併發環境的執行有序性的保證手段之一。當一段代碼會修改共享變量,這一段代碼成爲互斥區或 臨界區,爲了保證共享變量的正確性,synchronized標示了臨界區。典型的用法以下:

Java代碼          

  1. synchronized(鎖){  

  2.      臨界區代碼  

  3. }   

synchronized(鎖){
     臨界區代碼
}

 


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

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

public synchronized void add(int num) {
     balance = balance + num;
}
public synchronized void withdraw(int num) {
     balance = balance - num;
}

 


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

Java代碼          

  1. synchronized(鎖){  

  2. 臨界區代碼  

  3. }  

synchronized(鎖){
臨界區代碼
}


那麼對於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. }  

public class ThreadTest{
  public void test(){
     Object lock=new Object();
     synchronized (lock){
        //do something
     }
  }
}


 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來的,因此任意對象都有這個方法。看這個代碼片斷:

Java代碼          

  1. Object lock=new Object();//聲明瞭一個對象做爲鎖  

  2.    synchronized (lock) {  

  3.        balance = balance - num;  

  4.        //這裏放棄了同步鎖,好不容易獲得,又放棄了  

  5.        lock.wait();  

  6. }  

Object lock=new Object();//聲明瞭一個對象做爲鎖
   synchronized (lock) {
       balance = balance - num;
       //這裏放棄了同步鎖,好不容易獲得,又放棄了
       lock.wait();
}

 


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

 

Java代碼          

  1. import java.util.ArrayList;  

  2. import java.util.List;  

  3.   

  4. public class Plate {  

  5.   

  6.     List<Object> eggs = new ArrayList<Object>();  

  7.   

  8.     public synchronized Object getEgg() {  

  9.         while(eggs.size() == 0) {  

  10.             try {  

  11.                 wait();  

  12.             } catch (InterruptedException e) {  

  13.             }  

  14.         }  

  15.   

  16.         Object egg = eggs.get(0);  

  17.         eggs.clear();// 清空盤子  

  18.         notify();// 喚醒阻塞隊列的某線程到就緒隊列  

  19.         System.out.println("拿到雞蛋");  

  20.         return egg;  

  21.     }  

  22.   

  23.     public synchronized void putEgg(Object egg) {  

  24.         while(eggs.size() > 0) {  

  25.             try {  

  26.                 wait();  

  27.             } catch (InterruptedException e) {  

  28.             }  

  29.         }  

  30.         eggs.add(egg);// 往盤子裏放雞蛋  

  31.         notify();// 喚醒阻塞隊列的某線程到就緒隊列  

  32.         System.out.println("放入雞蛋");  

  33.     }  

  34.       

  35.     static class AddThread extends Thread{  

  36.         private Plate plate;  

  37.         private Object egg=new Object();  

  38.         public AddThread(Plate plate){  

  39.             this.plate=plate;  

  40.         }  

  41.           

  42.         public void run(){  

  43.             for(int i=0;i<5;i++){  

  44.                 plate.putEgg(egg);  

  45.             }  

  46.         }  

  47.     }  

  48.       

  49.     static class GetThread extends Thread{  

  50.         private Plate plate;  

  51.         public GetThread(Plate plate){  

  52.             this.plate=plate;  

  53.         }  

  54.           

  55.         public void run(){  

  56.             for(int i=0;i<5;i++){  

  57.                 plate.getEgg();  

  58.             }  

  59.         }  

  60.     }  

  61.       

  62.     public static void main(String args[]){  

  63.         try {  

  64.             Plate plate=new Plate();  

  65.             Thread add=new Thread(new AddThread(plate));  

  66.             Thread get=new Thread(new GetThread(plate));  

  67.             add.start();  

  68.             get.start();  

  69.             add.join();  

  70.             get.join();  

  71.         } catch (InterruptedException e) {  

  72.             e.printStackTrace();  

  73.         }  

  74.         System.out.println("測試結束");  

  75.     }  

  76. }  

import java.util.ArrayList;
import java.util.List;

public class Plate {

    List<Object> eggs = new ArrayList<Object>();

    public synchronized Object getEgg() {
        while(eggs.size() == 0) {
            try {
                wait();
            } catch (InterruptedException e) {
            }
        }

        Object egg = eggs.get(0);
        eggs.clear();// 清空盤子
        notify();// 喚醒阻塞隊列的某線程到就緒隊列
        System.out.println("拿到雞蛋");
        return egg;
    }

    public synchronized void putEgg(Object egg) {
        while(eggs.size() > 0) {
            try {
                wait();
            } catch (InterruptedException e) {
            }
        }
        eggs.add(egg);// 往盤子裏放雞蛋
        notify();// 喚醒阻塞隊列的某線程到就緒隊列
        System.out.println("放入雞蛋");
    }
    
    static class AddThread extends Thread{
        private Plate plate;
        private Object egg=new Object();
        public AddThread(Plate plate){
            this.plate=plate;
        }
        
        public void run(){
            for(int i=0;i<5;i++){
                plate.putEgg(egg);
            }
        }
    }
    
    static class GetThread extends Thread{
        private Plate plate;
        public GetThread(Plate plate){
            this.plate=plate;
        }
        
        public void run(){
            for(int i=0;i<5;i++){
                plate.getEgg();
            }
        }
    }
    
    public static void main(String args[]){
        try {
            Plate plate=new Plate();
            Thread add=new Thread(new AddThread(plate));
            Thread get=new Thread(new GetThread(plate));
            add.start();
            get.start();
            add.join();
            get.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("測試結束");
    }
}

  執行結果:

Html代碼   複製代碼    收藏代碼  

  1. 放入雞蛋  

  2. 拿到雞蛋  

  3. 放入雞蛋  

  4. 拿到雞蛋  

  5. 放入雞蛋  

  6. 拿到雞蛋  

  7. 放入雞蛋  

  8. 拿到雞蛋  

  9. 放入雞蛋  

  10. 拿到雞蛋  

  11. 測試結束  

放入雞蛋
拿到雞蛋
放入雞蛋
拿到雞蛋
放入雞蛋
拿到雞蛋
放入雞蛋
拿到雞蛋
放入雞蛋
拿到雞蛋
測試結束

 

 


聲明一個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. }  

public class VolatileTest{
  public volatile int a;
  public void add(int count){
       a=a+count;
  }
}

 


        當一個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. }  

public class VolatileTest{
  public volatile int a;
  public void setA(int a){
      this.a=a;
  }
}

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

相關文章
相關標籤/搜索