JAVA中關鍵字synchronized 和volatile的區別及用法



轉載至http://blog.sae.sina.com.cn/archives/5510

一,volatile關鍵字的可見性

要想理解volatile關鍵字,得先了解下JAVA的內存模型,Java內存模型的抽象示意圖如下:

從圖中可以看出:

每個線程都有一個自己的本地內存空間--線程棧空間???線程執行時,先把變量從主內存讀取到線程自己的本地內存空間,然後再對該變量進行操作

對該變量操作完後,在某個時間再把變量刷新回主內存

關於JAVA內存模型,更詳細的可參考: 深入理解Java內存模型(一)——基礎

因此,就存在內存可見性問題,看一個示例程序:(摘自書上)

複製代碼

 1publicclass RunThreadextends Thread {

 2

 3    privateboolean isRunning =true;

 4

 5    publicboolean isRunning() {

 6        return isRunning;

 7    }

 8

 9    publicvoid setRunning(boolean isRunning) {

10        this.isRunning = isRunning;

11    }

12

13    @Override

14    publicvoid run() {

15        System.out.println("進入到run方法中了");

16        while (isRunning ==true) {

17        }

18        System.out.println("線程執行完成了");

19    }

20 }

21

22publicclass Run {

23    publicstaticvoid main(String[] args) {

24        try {

25            RunThread thread = new RunThread();

26            thread.start();

27            Thread.sleep(1000);

28            thread.setRunning(false);

29        } catch (InterruptedException e) {

30            e.printStackTrace();

31        }

32    }

33 }

複製代碼

Run.java28行,main線程將啓動的線程RunThread中的共享變量設置爲false,從而想讓RunThread.java14行中的while循環結束。

如果,我們使用JVM -server參數執行該程序時,RunThread線程並不會終止!從而出現了死循環!!

原因分析:

現在有兩個線程,一個是main線程,另一個是RunThread。它們都試圖修改第三行的 isRunning變量。按照JVM內存模型,main線程將isRunning讀取到本地線程內存空間,修改後,再刷新回主內存。

而在JVM設置成 -server模式運行程序時,線程會一直在私有堆棧中讀取isRunning變量。因此,RunThread線程無法讀到main線程改變的isRunning變量

從而出現了死循環,導致RunThread無法終止。這種情形,在《Effective JAVA》中,將之稱爲活性失敗

解決方法,在第三行代碼處用 volatile關鍵字修飾即可。這裏,它強制線程從主內存中取 volatile修飾的變量。

   volatileprivateboolean isRunning =true;

 

擴展一下,當多個線程之間需要根據某個條件確定哪個線程可以執行時,要確保這個條件在線程之間是可見的。因此,可以用volatile修飾。

綜上,volatile關鍵字的作用是:使變量在多個線程間可見(可見性)

 

二,volatile關鍵字的非原子性

所謂原子性,就是某系列的操作步驟要麼全部執行,要麼都不執行。

比如,變量的自增操作 i++,分三個步驟:

從內存中讀取出變量 i的值

i的值加1

1後的值寫回內存

這說明 i++並不是一個原子操作。因爲,它分成了三步,有可能當某個線程執行到了第時被中斷了,那麼就意味着只執行了其中的兩個步驟,沒有全部執行。

關於volatile的非原子性,看個示例:

複製代碼

 1publicclass MyThreadextends Thread {

 2    publicvolatilestaticint count;

 3

 4    privatestaticvoid addCount() {

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

 6            count++;

 7        }

 8        System.out.println("count="+ count);

 9    }

10

11    @Override

12    publicvoid run() {

13        addCount();

14    }

15 }

16

17publicclass Run {

18    publicstaticvoid main(String[] args) {

19        MyThread[] mythreadArray = new MyThread[100];

20        for (int i = 0; i < 100; i++) {

21            mythreadArray[i] = new MyThread();

22        }

23

24        for (int i = 0; i < 100; i++) {

25            mythreadArray[i].start();

26        }

27    }

28 }

複製代碼

MyThread類第2行,count變量使用volatile修飾

Run.java20 for循環中創建了100個線程,第25行將這100個線程啓動去執行 addCount(),每個線程執行100次加1

期望的正確的結果應該是 100*100=10000,但是,實際上count並沒有達到10000

原因是:volatile修飾的變量並不保證對它的操作(自增)具有原子性。(對於自增操作,可以使用JAVA的原子類AutoicInteger類保證原子自增)

比如,假設 i自增到 5,線程A從主內存中讀取i,值爲5,將它存儲到自己的線程空間中,執行加1操作,值爲6。此時,CPU切換到線程B執行,從主從內存中讀取變量i的值。由於線程A還沒有來得及將加1後的結果寫回到主內存,線程B就已經從主內存中讀取了i,因此,線程B讀到的變量 i 值還是5

相當於線程B讀取的是已經過時的數據了,從而導致線程不安全性。這種情形在《Effective JAVA》中稱之爲安全性失敗

綜上,僅靠volatile不能保證線程的安全性。(原子性)

 

此外,volatile關鍵字修飾的變量不會被指令重排序優化。這裏以《深入理解JAVA虛擬機》中一個例子來說明下自己的理解:

線程A執行的操作如下:

複製代碼

Map configOptions ;

char[] configText;

 

volatile boolean initialized = false;

 

//線程A首先從文件中讀取配置信息,調用process...處理配置信息,處理完成了將initialized設置爲true

configOptions = newHashMap();

configText =readConfigFile(fileName);

processConfig(configText,configOptions);//負責將配置信息configOptions成功初始化

initialized = true;

複製代碼

 

線程B等待線程A把配置信息初始化成功後,使用配置信息去幹活.....線程B執行的操作如下:

複製代碼

while(!initialized)

{

   sleep();

}

 

//使用配置信息幹活

doSomethingWithConfig();

複製代碼

 

如果initialized變量不用 volatile 修飾,在線程A執行的代碼中就有可能指令重排序。

即:線程A執行的代碼中的最後一行:initialized = true 重排序到了 processConfig方法調用的前面執行了,這就意味着:配置信息還未成功初始化,但是initialized變量已經被設置成true了。那麼就導致線程Bwhile循環提前跳出,拿着一個還未成功初始化的配置信息去幹活(doSomethingWithConfig方法)。。。。

因此,initialized變量就必須得用 volatile修飾。這樣,就不會發生指令重排序,也即:只有當配置信息被線程A成功初始化之後,initialized變量纔會初始化爲true綜上,volatile修飾的變量會禁止指令重排序(有序性)

 

三,volatile synchronized的比較

volatile主要用在多個線程感知實例變量被更改了場合,從而使得各個線程獲得最新的值。它強制線程每次從主內存中講到變量,而不是從線程的私有內存中讀取變量,從而保證了數據的可見性。

關於synchronized,可參考:JAVA多線程之Synchronized關鍵字--對象鎖的特點

比較:

volatile輕量級,只能修飾變量。synchronized重量級,還可修飾方法

volatile只能保證數據的可見性,不能用來同步,因爲多個線程併發訪問volatile修飾的變量不會阻塞。

synchronized不僅保證可見性,而且還保證原子性,因爲,只有獲得了鎖的線程才能進入臨界區,從而保證臨界區中的所有語句都全部執行。多個線程爭搶synchronized鎖對象時,會出現阻塞。

 

四,線程安全性

線程安全性包括兩個方面,可見性。原子性。

從上面自增的例子中可以看出:僅僅使用volatile並不能保證線程安全性。而synchronized則可實現線程的安全性。

 

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