volatile原理與技巧--Java

 

爲何使用volatile比同步代價更低?java

同步的代價, 主要由其覆蓋範圍決定, 若是能夠下降同步的覆蓋範圍, 則能夠大幅提高程序性能. 算法

而volatile的覆蓋範圍僅僅變量級別的. 所以它的同步代價很低.數組

volatile原理是什麼?多線程

volatile的語義, 實際上是告訴處理器, 不要將我放入工做內存, 請直接在主存操做我.(工做內存詳見java內存模型)併發

所以, 當多核或多線程在訪問該變量時, 都將直接操做主存, 這從本質上, 作到了變量共享.性能

volatile的有什麼優點?學習

1, 更大的程序吞吐量this

2, 更少的代碼實現多線程線程

3, 程序的伸縮性較好設計

4, 比較好理解, 無需過高的學習成本

volatile有什麼劣勢?

1, 容易出問題

2, 比較難設計

volatile運算存在髒數據問題

volatile僅僅能保證變量可見性, 沒法保證原子性.

volatile的race condition示例:

public class TestRaceCondition {

    private volatile int i = 0;

   

    public void increase() {

       i++;

    }

    public int getValue() {

       return i;

    }

}

當多線程執行increase方法時, 是否能保證它的值會是線性遞增的呢?

答案是否認的.

緣由:

這裏的increase方法, 執行的操做是i++, 即 i = i + 1;

針對i = i + 1, 在多線程中的運算, 自己須要改變i的值.

若是, 在i已從內存中取到最新值, 但未與1進行運算, 此時其餘線程已數次將運算結果賦值給i.

則當前線程結束時, 以前的數次運算結果都將被覆蓋.

即, 執行100次increase, 可能結果是 < 100.

通常來講, 這種狀況須要較高的壓力與併發狀況下, 纔會出現.

如何避免這種狀況?

解決以上問題的方法:

一種是 操做時, 加上同步.

這種方法, 無疑將大大下降程序性能, 且違背了volatile的初衷.

第二種方式是, 使用硬件原語(CAS), 實現非阻塞算法

從CPU原語上,  支持變量級別的低開銷同步.

CPU原語-比較並交換(CompareAndSet),實現非阻塞算法

什麼是CAS?

cas是現代CPU提供給併發程序使用的原語操做. 不一樣的CPU有不一樣的使用規範.

在 Intel 處理器中,比較並交換經過指令的 cmpxchg 系列實現。

PowerPC 處理器有一對名爲「加載並保留」和「條件存儲」的指令,它們實現相同的目地;

MIPS 與 PowerPC 處理器類似,除了第一個指令稱爲「加載連接」。

CAS 操做包含三個操做數 —— 內存位置(V)、預期原值(A)和新值(B)

什麼是非阻塞算法?

一個線程的失敗或掛起不該該影響其餘線程的失敗或掛起.這類算法稱之爲非阻塞(nonblocking)算法

對比阻塞算法:

若是有一類併發操做, 其中一個線程優先獲得對象監視器的鎖, 當其餘線程到達同步邊界時, 就會被阻塞.

直到前一個線程釋放掉鎖後, 才能夠繼續競爭對象鎖.(固然,這裏的競爭也但是公平的, 按先來後到的次序)

CAS 原理:

我認爲位置 V 應該包含值 A;若是包含該值,則將 B 放到這個位置;不然,不要更改該位置,只告訴我這個位置如今的值便可。

CAS使用示例(jdk 1.5 併發包 AtomicInteger類分析:)

    /**

     * Atomically sets to the given value and returns the old value.

     *

     * @param newValue the new value

     * @return the previous value

     */

    public final int getAndSet(int newValue) {

        for (;;) {

            int current = get();

            if (compareAndSet(current, newValue))

                return current;

        }

    }

    public final boolean compareAndSet(int expect, int update) {

        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);

    }

這個方法是, AtomicInteger類的經常使用方法, 做用是, 將變量設置爲指定值, 並返回設置前的值.

它利用了cpu原語compareAndSet來保障值的惟一性.

另, AtomicInteger類中, 其餘的實用方法, 也是基於一樣的實現方式.

好比 getAndIncrement, getAndDecrement, getAndAdd等等.

CAS語義上存在的"ABA 問題"

什麼是ABA問題?

假設, 第一次讀取V地址的A值, 而後經過CAS來判斷V地址的值是否仍舊爲A, 若是是, 就將B的值寫入V地址,覆蓋A值.

可是, 語義上, 有一個漏洞, 當第一次讀取V的A值, 此時, 內存V的值變爲B值, 而後在未執行CAS前, 又變回了A值.

此時, CAS再執行時, 會判斷其正確的, 並進行賦值.

這種判斷值的方式來判定內存是否被修改過, 針對某些問題, 是不適用的.

爲了解決這種問題, jdk 1.5併發包提供了

AtomicStampedReference

(有標記的原子引用)類, 經過控制變量值的版原本保證CAS正確性.

其實, 大部分經過值的變化來CAS, 已經夠用了.

jdk1.5原子包介紹(基於volatile)

包的特點:

1, 普通原子數值類型AtomicInteger, AtomicLong提供一些原子操做的加減運算.

2, 使用瞭解決髒數據問題的經典模式-"比對後設定", 即 查看主存中數據是否與預期提供的值一致,若是一致,才更新.

3, 使用AtomicReference能夠實現對全部對象的原子引用及賦值.包括Double與Float,

但不包括對其的計算.浮點的計算,只能依靠同步關鍵字或Lock接口來實現了.

4, 對數組元素裏的對象,符合以上特色的, 也可採用原子操做.包裏提供了一些數組原子操做類

AtomicIntegerArray, AtomicLongArray等等.

5, 大幅度提高系統吞吐量及性能.

相關文章
相關標籤/搜索