java線程安全

轉載http://blog.csdn.net/yangaming/article/details/8634806

java的線程安全的問題:其實就是控制多個線程對某個資源的有序訪問或者修改。

java的內存模型主要是解決兩個問題:(1)可見性(2)有序性

(1)可見性:多個線程之間是不能相互傳遞數據通信的,他們之間只能通過共享變量來進行溝通。java的內存模型規定了jvm有主存,主存是多個線程共享的。當new一個對象的時候,也是被分配在主存當中,每個線程都有自己的工作內存,工作線程存儲了主存的某些對象的副本,淡然縣城的工作內存大小是有限制的。

當線程操作某個對象的時候,執行的順序如下:

(1)從主存複製變量到當前工作內存(read and load)

(2)執行代碼,改變共享變量值(use and assign )

(3)用工作內存的數據刷新主存的相關內容(store and write)


(3) 用工作內存數據刷新主存相關內容 (store and write)

JVM規範定義了線程對主存的操作指令:read,load,use,assign,store,write。當一個共享變量在多個線程的工作

內存中都有副本時,如果一個線程修改了這個共享變量,那麼其他線程應該能夠看到這個被修改後的值,這就是多線程的可見性問題。

(2)有序性:線程在引用變量的時候不能直接從主存中引用,如果線程的工作內存中沒有改變量,則會從主存中拷貝一個副本到工作內存中。這個過程爲read-load,完成後線程會引用該副本。當同一線程再度引用該字段的時候,有可能重新從主存中獲取變量的副本(read-load-use),也有可能直接引用原來的副本(use).也就是說read/load/use順序可以由Jvm實現系統決定。線程不能直接爲主存中的字段賦值,它會將值指定給工作內存中的變量副本(assign),

完成後這個變量副本會同步到主存儲區(store-write),至於何時同步過去,根據JVM實現系統決定.有該字段,則會從主

內存中將該字段賦值到工作內存中,這個過程爲read-load,完成後線程會引用該變量副本,

當同一線程多次重複對字段賦值時,比如:

[java] view plain copy
  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] view plain copy
  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關鍵字 

 上面說了,javasynchronized關鍵字做爲多線程併發環境的執行有序性的保證手段之一。當一段代碼會修改共享變量,

這一段代碼成爲互斥區或臨界區,爲了保證共享變量的正確性,synchronized標示了臨界區。典型的用法如下:

[java] view plain copy
  1. synchronized(鎖){    
  2.      臨界區代碼    
  3. }   

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

[java] view plain copy
  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的用法是這樣的嗎:

[java] view plain copy
  1. synchronized(鎖){    
  2. 臨界區代碼    
  3. }    

那麼對於public synchronized void add(int num)這種情況,意味着什麼呢?其實這種情況,鎖就是這個方法所在的對象。

同理,如果方法是public  static synchronized void add(intnum),那麼鎖就是這個方法所在的class


       
理論上,每個對象都可以做爲鎖,但一個對象做爲鎖時,應該被多個線程共享,這樣才顯得有意義,在併發環境下,

一個沒有共享的對象作爲鎖是沒有意義的。假如有這樣的代碼:

[java] view plain copy
  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來的,所以任意對象都有這個方法。看這個代碼片段:

[java] view plain copy
  1. Object lock=new Object();//聲明瞭一個對象作爲鎖    
  2.    synchronized (lock) {    
  3.        balance = balance - num;    
  4.        //這裏放棄了同步鎖,好不容易得到,又放棄了    
  5.        lock.wait();    
  6. }    

    如果一個線程獲得了鎖lock,進入了同步塊,執行lock.wait(),那麼這個線程會進入到lock的阻塞隊列。如果調用

lock.notify()則會通知阻塞隊列的某個線程進入就緒隊列。

聲明一個盤子,只能放一個雞蛋

[java] view plain copy
  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.         if (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.         if (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. }   

執行結果:

synchronized關鍵字 

 上面說了,javasynchronized關鍵字做爲多線程併發環境的執行有序性的保證手段之一。當一段代碼會修改共享變量,

這一段代碼成爲互斥區或臨界區,爲了保證共享變量的正確性,synchronized標示了臨界區。典型的用法如下:

[java] view plain copy
  1. synchronized(鎖){    
  2.      臨界區代碼    
  3. }   

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

[java] view plain copy
  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的用法是這樣的嗎:

[java] view plain copy
  1. synchronized(鎖){    
  2. 臨界區代碼    
  3. }    

那麼對於public synchronized void add(int num)這種情況,意味着什麼呢?其實這種情況,鎖就是這個方法所在的對象。

同理,如果方法是public  static synchronized void add(intnum),那麼鎖就是這個方法所在的class


       
理論上,每個對象都可以做爲鎖,但一個對象做爲鎖時,應該被多個線程共享,這樣才顯得有意義,在併發環境下,

一個沒有共享的對象作爲鎖是沒有意義的。假如有這樣的代碼:

[java] view plain copy
  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來的,所以任意對象都有這個方法。看這個代碼片段:

[java] view plain copy
  1. Object lock=new Object();//聲明瞭一個對象作爲鎖    
  2.    synchronized (lock) {    
  3.        balance = balance - num;    
  4.        //這裏放棄了同步鎖,好不容易得到,又放棄了    
  5.        lock.wait();    
  6. }    

    如果一個線程獲得了鎖lock,進入了同步塊,執行lock.wait(),那麼這個線程會進入到lock的阻塞隊列。如果調用

lock.notify()則會通知阻塞隊列的某個線程進入就緒隊列。

聲明一個盤子,只能放一個雞蛋

[java] view plain copy
  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.         if (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.         if (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. }   

執行結果:

1. 放入雞蛋  

2. 拿到雞蛋  

3. 放入雞蛋  

4. 拿到雞蛋  

5. 放入雞蛋  

6. 拿到雞蛋  

7. 放入雞蛋  

8. 拿到雞蛋  

9. 放入雞蛋  

10.拿到雞蛋  

測試結束 

聲明一個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關鍵字

volatilejava提供的一種同步手段,只不過它是輕量級的同步,爲什麼這麼說,因爲volatile只能保證多線程的內存可見性,

不能保證多線程的執行有序性。而最徹底的同步要保證有序性和可見性,例如synchronized。任何被volatile修飾的變量,

都不拷貝副本到工作內存,任何修改都及時寫在主存。因此對於Valatile修飾的變量的修改,所有線程馬上就能看到,

但是volatile不能保證對變量的修改是有序的。什麼意思呢?假如有這樣的代碼:

[java] view plain copy
  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] view plain copy
  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的開銷將會非常小。




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