volatile, 用更低的代價(jià)替代同步
為什么使用volatile比同步代價(jià)更低? 同步的代價(jià), 主要由其覆蓋范圍決定, 如果可以降低同步的覆蓋范圍, 則可以大幅提升程序性能.
而volatile的覆蓋范圍僅僅變量級(jí)別的. 因此它的同步代價(jià)很低.
volatile原理是什么? volatile的語(yǔ)義, 其實(shí)是告訴處理器, 不要將我放入工作內(nèi)存, 請(qǐng)直接在主存操作我.(工作內(nèi)存詳見(jiàn)java內(nèi)存模型)
因此, 當(dāng)多核或多線程在訪問(wèn)該變量時(shí), 都將直接操作主存, 這從本質(zhì)上, 做到了變量共享.
volatile的有什么優(yōu)勢(shì)? 1, 更大的程序吞吐量 2, 更少的代碼實(shí)現(xiàn)多線程 3, 程序的伸縮性較好 4, 比較好理解, 無(wú)需太高的學(xué)習(xí)成本
volatile有什么劣勢(shì)? 1, 容易出問(wèn)題 2, 比較難設(shè)計(jì)
volatile運(yùn)算存在臟數(shù)據(jù)問(wèn)題
volatile僅僅能保證變量可見(jiàn)性, 無(wú)法保證原子性.
volatile的race condition示例:
public class TestRaceCondition { private volatile int i = 0; public void increase() { i++; }
public int getValue() { return i; } }
當(dāng)多線程執(zhí)行increase方法時(shí), 是否能保證它的值會(huì)是線性遞增的呢? 答案是否定的.
原因: 這里的increase方法, 執(zhí)行的操作是i++, 即 i = i + 1; 針對(duì)i = i + 1, 在多線程中的運(yùn)算, 本身需要改變i的值. 如果, 在i已從內(nèi)存中取到最新值, 但未與1進(jìn)行運(yùn)算, 此時(shí)其他線程已數(shù)次將運(yùn)算結(jié)果賦值給i. 則當(dāng)前線程結(jié)束時(shí), 之前的數(shù)次運(yùn)算結(jié)果都將被覆蓋.
即, 執(zhí)行100次increase, 可能結(jié)果是 < 100. 一般來(lái)說(shuō), 這種情況需要較高的壓力與并發(fā)情況下, 才會(huì)出現(xiàn).
如何避免這種情況? 解決以上問(wèn)題的方法: 一種是 操作時(shí), 加上同步. 這種方法, 無(wú)疑將大大降低程序性能, 且違背了volatile的初衷.
第二種方式是, 使用硬件原語(yǔ)(CAS), 實(shí)現(xiàn)非阻塞算法 從CPU原語(yǔ)上, 支持變量級(jí)別的低開(kāi)銷同步.
CPU原語(yǔ)-比較并交換(CompareAndSet),實(shí)現(xiàn)非阻塞算法
什么是CAS? cas是現(xiàn)代CPU提供給并發(fā)程序使用的原語(yǔ)操作. 不同的CPU有不同的使用規(guī)范.
在 Intel 處理器中,比較并交換通過(guò)指令的 cmpxchg 系列實(shí)現(xiàn)。 PowerPC 處理器有一對(duì)名為“加載并保留”和“條件存儲(chǔ)”的指令,它們實(shí)現(xiàn)相同的目地; MIPS 與 PowerPC 處理器相似,除了第一個(gè)指令稱為“加載鏈接”。
CAS 操作包含三個(gè)操作數(shù) —— 內(nèi)存位置(V)、預(yù)期原值(A)和新值(B)
什么是非阻塞算法? 一個(gè)線程的失敗或掛起不應(yīng)該影響其他線程的失敗或掛起.這類算法稱之為非阻塞(nonblocking)算法
對(duì)比阻塞算法: 如果有一類并發(fā)操作, 其中一個(gè)線程優(yōu)先得到對(duì)象監(jiān)視器的鎖, 當(dāng)其他線程到達(dá)同步邊界時(shí), 就會(huì)被阻塞. 直到前一個(gè)線程釋放掉鎖后, 才可以繼續(xù)競(jìng)爭(zhēng)對(duì)象鎖.(當(dāng)然,這里的競(jìng)爭(zhēng)也可是公平的, 按先來(lái)后到的次序)
CAS 原理:
我認(rèn)為位置 V 應(yīng)該包含值 A;如果包含該值,則將 B 放到這個(gè)位置;否則,不要更改該位置,只告訴我這個(gè)位置現(xiàn)在的值即可。
CAS使用示例(jdk 1.5 并發(fā)包 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); }
這個(gè)方法是, AtomicInteger類的常用方法, 作用是, 將變量設(shè)置為指定值, 并返回設(shè)置前的值. 它利用了cpu原語(yǔ)compareAndSet來(lái)保障值的唯一性.
另, AtomicInteger類中, 其他的實(shí)用方法, 也是基于同樣的實(shí)現(xiàn)方式. 比如 getAndIncrement, getAndDecrement, getAndAdd等等.
CAS語(yǔ)義上存在的"ABA 問(wèn)題"
什么是ABA問(wèn)題? 假設(shè), 第一次讀取V地址的A值, 然后通過(guò)CAS來(lái)判斷V地址的值是否仍舊為A, 如果是, 就將B的值寫入V地址,覆蓋A值.
但是, 語(yǔ)義上, 有一個(gè)漏洞, 當(dāng)?shù)谝淮巫x取V的A值, 此時(shí), 內(nèi)存V的值變?yōu)锽值, 然后在未執(zhí)行CAS前, 又變回了A值. 此時(shí), CAS再執(zhí)行時(shí), 會(huì)判斷其正確的, 并進(jìn)行賦值.
這種判斷值的方式來(lái)斷定內(nèi)存是否被修改過(guò), 針對(duì)某些問(wèn)題, 是不適用的.
為了解決這種問(wèn)題, jdk 1.5并發(fā)包提供了AtomicStampedReference (有標(biāo)記的原子引用)類, 通過(guò)控制變量值的版本來(lái)保證CAS正確性.
其實(shí), 大部分通過(guò)值的變化來(lái)CAS, 已經(jīng)夠用了.
jdk1.5原子包介紹(基于volatile)
包的特色: 1, 普通原子數(shù)值類型AtomicInteger, AtomicLong提供一些原子操作的加減運(yùn)算.
2, 使用了解決臟數(shù)據(jù)問(wèn)題的經(jīng)典模式-"比對(duì)后設(shè)定", 即 查看主存中數(shù)據(jù)是否與預(yù)期提供的值一致,如果一致,才更新.
3, 使用AtomicReference可以實(shí)現(xiàn)對(duì)所有對(duì)象的原子引用及賦值.包括Double與Float, 但不包括對(duì)其的計(jì)算.浮點(diǎn)的計(jì)算,只能依靠同步關(guān)鍵字或Lock接口來(lái)實(shí)現(xiàn)了.
4, 對(duì)數(shù)組元素里的對(duì)象,符合以上特點(diǎn)的, 也可采用原子操作.包里提供了一些數(shù)組原子操作類 AtomicIntegerArray, AtomicLongArray等等.
5, 大幅度提升系統(tǒng)吞吐量及性能.
具體使用, 詳解java doc.
|