關於volatile和同步相關的東西,網上有太多錯誤和解釋不清的東西, 所以查閱相關書籍和文章后總結如下, 如果還是也存在不正確的內容,請一定要指出來, 以免誤人子弟:)
1. 原子性與可視性
原子性是指操作不能被線程調度機制中斷, 除long和double之外的所有基本類型的讀或寫操作都是原子操作,注意這里說的讀寫, 僅指如return i, i = 10, 對於像i++這種操作,包含了讀,加1,寫指令,所以不是原子操作。 對於long和double的讀寫,在64位JVM上會把它們當作兩個32位來操作,所以不具備原子性。
在定義long和double類型變量時,如果使用volatile來修飾,那么也可以獲得原子性,除此以外,volatile與原子性沒有直接關系。
可視性,volatile的主要作用就是確保可視性,那么什么是可視性?
在系統中(多處理器更加明顯),對某一變量的修改有時會暫時保存在本地處理器的緩存中,還沒有寫入共享內存,這時候有另外一個線程讀取變量在共享內存的值,那么這個修改對這個線程就是不可視的。
Volatile修飾的成員變量在每次被線程訪問時,都強迫從共享內存中重讀該成員變量的值。而且,當成員變量發生變化時,強迫線程將變化值直接寫到共享內存。這樣在任何時刻,兩個不同的線程總是看到某個成員變量的同一個值。 原子操作不一定就有可視性, 比如賦值,i = 10, 如果i沒有被特別修飾, 那么因為緩存的原因, 它仍然可能是不可視的
所以原子性和可視性是完全不同的兩個概念
2. volatile的應用場景
詳細可以參考java語言架構師Brain Geotz的文章
Java 中volatile 變量可以被看作是一種 “程度較輕的 synchronized”;與 synchronized 塊相比,volatile 優點是所需的編碼較少,並且運行時開銷也較少, 不會引起線程阻塞。Volatile 變量具有 synchronized 的可視性特性,但是不具備原子特性。
這就導致Volatile 變量可用於提供線程安全,但是應用場景非常有限,在一些經典java書里,基本都不推薦使用volatile替代synchronized來實現同步,因為風險較大, 很容易出錯。
Brain給出的使用volatile實現線程安全的條件:
對變量的寫操作不依賴於當前值。 (count++這種就不行了)
該變量沒有包含在具有其他變量的不變式中(Invariants,例如 “start <=end”)。
我的理解是, 這兩個條件都是因為volatile不能提供原子性導致的, 如果多線程執行的一個操作不是原子性的, 使用volatile時就一定要慎重。
如果滿足這兩個條件, 多線程執行的操作是原子性的, 那就是可以使用,如:
將 volatile 變量作為狀態標志使用
volatile boolean shutdownRequested; . ... public void shutdown() { shutdownRequested = true; } //不依賴當前值,原子操作 public void doWork() { while (!shutdownRequested) { // do stuff } }
文章中的其他幾種模式, 也都差不多這個意思。
還有一種情況,如果讀操作遠遠超過寫操作,可以結合使用內部鎖和 volatile 變量來減少公共代碼路徑的開銷。
結合使用 volatile 和 synchronized 實現 “開銷較低的讀-寫鎖”
@ThreadSafe public class CheesyCounter { // Employs the cheap read-write lock trick // All mutative operations MUST be done with the 'this' lock held @GuardedBy("this") private volatile int value; public int getValue() { return value; } //使用volatile替代synchronized public synchronized int increment() { return value++; }
然而,你可以在讀操作中使用 volatile 確保當前值的可見性,因此可以使用鎖進行所有變化的操作,使用 volatile 進行只讀操作。其中,鎖一次只允許一個線程訪問值,volatile 允許多個線程執行讀操作,因此當使用 volatile 保證讀代碼路徑時,要比使用鎖執行全部代碼路徑獲得更高的共享度 —— 就像讀-寫操作一樣。
3. synchronized
class AtomTest implements Runnable { private volatile int i = 0; public int getVal() {return i;} public synchronized void inc() {i++; i++;} @Override public void run() { while (true) { inc(); } } } public class TestThread { public static void main(String[] args) throws InterruptedException { ExecutorService exec = Executors.newCachedThreadPool(); AtomTest at = new AtomTest(); exec.execute(at); while (true) { int val = at.getVal(); if (val % 2 != 0) { System.out.println(val); System.exit(0); } } } }
結果會輸出奇數, 退出程序, 原因是getVal讀到了inc的中間值。 這種情況只能在getVal方法前加synchronized
在讀取的時候也加鎖, 這樣在讀的時候如果正在寫, 那么等待, 所以就不會讀到inc的中間值。
關於synchronized值得注意的幾個點:
1) 所有對象都含有一個鎖,當調用到synchronized(object)塊時,先檢測obj有沒有加鎖,如果有, 阻塞, 如果沒有, 對object加鎖, 執行完后釋放鎖。
2) synchronized void f() {//...} 等價於 void f() { synchronized(this) {//...} }, 在當前對象上加鎖
3) synchronized 提供原子性和可視性, 被它完全保護的變量不需要用volatile
4) synchronized關鍵字是不能繼承的,也就是說,基類的方法synchronized f(){} 在繼承類中並不自動是synchronized f(){},而是變成了f(){}。繼承類需要你顯式的指定它的某個方法為synchronized方法。
注意第一點, 非常重要:雖然sync塊可以包裹一段代碼,但是鎖是加對象上,不是加在代碼上,它的工作機制如下:
對於synchronized(obj) {//...}: 先檢測obj有沒有加鎖,如果有, 阻塞, 如果沒有, 對obj加鎖, 執行塊中的代碼,完畢后釋放鎖。這里只檢測obj對象上的鎖,不關注代碼塊里的代碼或者對象。
所以, 加鎖的范圍由obj決定,理解了這一點, 下面的很多種情況就會很容易理解:
1. 當兩個並發線程訪問同一個對象object中的這個相同synchronized(this)同步代碼塊時,一個時間內針對該對象的操作只能有一個線程得到執行。另一個線程必須等待。
- 如果同一對象已經加鎖, 另一線程執行到sync塊,檢測到有鎖掛起。
2. 然而,另一個線程仍然可以訪問該object中的非synchronized(this)同步代碼塊。
- 非sync代碼,不關注對象是否加鎖
3. 當一個線程訪問object的一個synchronized(this)同步代碼塊時,其他線程對該object中所有其它synchronized(this)同步代碼塊的訪問將被阻塞。
- synchronized(this) 只是關注this對象, 只要this已加鎖,執行到同一對象中不同方法的sync塊時,也會阻塞。
4. 不同的對象實例的synchronized(this)方法是不相干擾的。也就是說,其它線程照樣可以同時訪問相同類的另一個對象實例中的synchronized方法。
- 可以同時訪問不同對象中的sync塊, 原因很簡單, 因為synchronized(this),關注的是this對象,不同對象的this是不一樣的。
5. 同理,也可以對其他對象加鎖,
1) 對於類中的成員對象: private Integer i = new Integer(0); synchronized(i) {//...} i創建在堆上,每個對象有一個i,所以效果與synchronized(this)一樣。
2) 對於靜態成員對象: private static Integer i = new Integer(0); synchronized(i) {//...}: i創建在靜態區,屬於類, 所以效果與synchronized(ClassName.class)一樣。
6.對類對象加鎖時,對該類的所有對象都起作用:synchronized(ClassName.class) {//...}
最后我在測試代碼時,發現對於private Integer i = 0; synchronized(i) {//...}
鎖的效果是全局的,推測可能是Integer對0進程打包時,自動生成的這個對象可能在常量區。 后來查了下資料才發現並非如此:
Integer實現中有一個IntegerCache類,它包含一個靜態的Integer數組,在類加載時就將-128 到 127 的Integer對象創建了,並保存在cache數組中,一旦程序調用valueOf 方法,如果i的值是在-128 到 127 之間就直接在cache緩存數組中去取Integer對象。 所以這里的i引用的是整個全局數組里值, 所以鎖也是全局的了。。。
如果改成private Integer i = 300, 然后加鎖就只在本對象有效了。 原因是i不在緩存范圍,所以創建在了堆上。
refer http://blog.csdn.net/xiaohai0504/article/details/6885137