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)多核或多線(xiàn)程在訪(fǎng)問(wèn)該變量時(shí), 都將直接
操作
主存, 這從本質(zhì)上, 做到了變量共享.
volatile的有什么優(yōu)勢(shì)?
1, 更大的程序吞吐量
2, 更少的代碼實(shí)現(xiàn)多線(xiàn)程
3, 程序的伸縮性較好
4, 比較好理解, 無(wú)需太高的學(xué)習(xí)成本
volatile有什么劣勢(shì)?
1, 容易出問(wèn)題
2, 比較難設(shè)計(jì)
?
?
在java線(xiàn)程并發(fā)處理中,有一個(gè)關(guān)鍵字volatile的使用目前存在很大的混淆,以為使用這個(gè)關(guān)鍵字,在進(jìn)行多線(xiàn)程并發(fā)處理的時(shí)候就可以萬(wàn)事大吉。
?
Java語(yǔ)言是支持多線(xiàn)程的,為了解決線(xiàn)程并發(fā)的問(wèn)題,在語(yǔ)言?xún)?nèi)部引入了 同步塊 和 volatile 關(guān)鍵字機(jī)制。
?
?
?
synchronized ?
?
同步塊大家都比較熟悉,通過(guò) synchronized 關(guān)鍵字來(lái)實(shí)現(xiàn),所有加上synchronized 和 塊語(yǔ)句,在多線(xiàn)程訪(fǎng)問(wèn)的時(shí)候,同一時(shí)刻只能有一個(gè)線(xiàn)程能夠用
?
synchronized 修飾的方法 或者 代碼塊。
?
volatile
?
用volatile修飾的變量,線(xiàn)程在每次使用變量的時(shí)候,都會(huì)讀取變量修改后的最的值。volatile很容易被誤用,用來(lái)進(jìn)行原子性操作。
?
下面看一個(gè)例子,我們實(shí)現(xiàn)一個(gè)計(jì)數(shù)器,每次線(xiàn)程啟動(dòng)的時(shí)候,會(huì)調(diào)用計(jì)數(shù)器inc方法,對(duì)計(jì)數(shù)器進(jìn)行加一
?執(zhí)行環(huán)境——jdk版本:jdk1.6.0_31 ,內(nèi)存 :3G?? cpu:x86 2.4G
運(yùn)行結(jié)果:Counter.count=995
?
?實(shí)際運(yùn)算結(jié)果每次可能都不一樣,本機(jī)的結(jié)果為:運(yùn)行結(jié)果:Counter.count=995,可以看出,在多線(xiàn)程的環(huán)境下,Counter.count并沒(méi)有期望結(jié)果是1000
?
?
很多人以為,這個(gè)是多線(xiàn)程并發(fā)問(wèn)題,只需要在變量count之前加上volatile就可以避免這個(gè)問(wèn)題,那我們?cè)谛薷拇a看看,看看結(jié)果是不是符合我們的期望
?
運(yùn)行結(jié)果:Counter.count=992
運(yùn)行結(jié)果還是沒(méi)有我們期望的1000,下面我們分析一下原因
?
?
?
在 java 垃圾回收整理一文中,描述了jvm運(yùn)行時(shí)刻內(nèi)存的分配。其中有一個(gè)內(nèi)存區(qū)域是jvm虛擬機(jī)棧,每一個(gè)線(xiàn)程運(yùn)行時(shí)都有一個(gè)線(xiàn)程棧,
?
線(xiàn)程棧保存了線(xiàn)程運(yùn)行時(shí)候變量值信息。當(dāng)線(xiàn)程訪(fǎng)問(wèn)某一個(gè)對(duì)象時(shí)候值的時(shí)候,首先通過(guò)對(duì)象的引用找到對(duì)應(yīng)在堆內(nèi)存的變量的值,然后把堆內(nèi)存
?
變量的具體值load到線(xiàn)程本地內(nèi)存中,建立一個(gè)變量副本,之后線(xiàn)程就不再和對(duì)象在堆內(nèi)存變量值有任何關(guān)系,而是直接修改副本變量的值,
?
在修改完之后的某一個(gè)時(shí)刻(線(xiàn)程退出之前),自動(dòng)把線(xiàn)程變量副本的值回寫(xiě)到對(duì)象在堆中變量。這樣在堆中的對(duì)象的值就產(chǎn)生變化了。?
?
read and load 從主存復(fù)制變量到當(dāng)前工作內(nèi)存
use and assign? 執(zhí)行代碼,改變共享變量值
?
store and write 用工作內(nèi)存數(shù)據(jù)刷新主存相關(guān)內(nèi)容
?
其中use and assign 可以多次出現(xiàn)
?
但是這一些操作并不是原子性,也就是 在read load之后,如果主內(nèi)存count變量發(fā)生修改之后,線(xiàn)程工作內(nèi)存中的值由于已經(jīng)加載,不會(huì)產(chǎn)生對(duì)應(yīng)的變化,所以計(jì)算出來(lái)的結(jié)果會(huì)和預(yù)期不一樣
?
對(duì)于volatile修飾的變量,jvm虛擬機(jī)只是保證從主內(nèi)存加載到線(xiàn)程工作內(nèi)存的值是最新的
?
例如假如線(xiàn)程1,線(xiàn)程2 在進(jìn)行read,load 操作中,發(fā)現(xiàn)主內(nèi)存中count的值都是5,那么都會(huì)加載這個(gè)最新的值
?
在線(xiàn)程1堆count進(jìn)行修改之后,會(huì)write到主內(nèi)存中,主內(nèi)存中的count變量就會(huì)變?yōu)?
?
線(xiàn)程2由于已經(jīng)進(jìn)行read,load操作,在進(jìn)行運(yùn)算之后,也會(huì)更新主內(nèi)存count的變量值為6
?
導(dǎo)致兩個(gè)線(xiàn)程及時(shí)用volatile關(guān)鍵字修改之后,還是會(huì)存在并發(fā)的情況。
?
?
如何避免這種情況?
解決以上問(wèn)題的方法:
一種是 操作時(shí), 加上同步.
這種方法, 無(wú)疑將大大降低程序性能, 且違背了volatile的初衷.
第二種方式是, 使用硬件原語(yǔ)(CAS), 實(shí)現(xiàn)非阻塞算法
從CPU原語(yǔ)上,? 支持變量級(jí)別的低開(kāi)銷(xiāo)同步.
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è)指令稱(chēng)為“加載鏈接”。
CAS 操作包含三個(gè)操作數(shù) —— 內(nèi)存位置(V)、預(yù)期原值(A)和新值(B)
什么是非阻塞算法?
一個(gè)線(xiàn)程的失敗或掛起不應(yīng)該影響其他線(xiàn)程的失敗或掛起.這類(lèi)算法稱(chēng)之為非阻塞(nonblocking)算法
對(duì)比阻塞算法:
如果有一類(lèi)并發(fā)操作, 其中一個(gè)線(xiàn)程優(yōu)先得到對(duì)象監(jiān)視器的鎖, 當(dāng)其他線(xiàn)程到達(dá)同步邊界時(shí), 就會(huì)被阻塞.
直到前一個(gè)線(xiàn)程釋放掉鎖后, 才可以繼續(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類(lèi)分析:)
?
?
???
這個(gè)方法是, AtomicInteger類(lèi)的常用方法, 作用是, 將變量設(shè)置為指定值, 并返回設(shè)置前的值.
它利用了cpu原語(yǔ)compareAndSet來(lái)保障值的唯一性.
另, AtomicInteger類(lèi)中, 其他的實(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的值寫(xiě)入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)記的原子引用)類(lèi), 通過(guò)控制變量值的版本來(lái)保證CAS正確性.
其實(shí), 大部分通過(guò)值的變化來(lái)CAS, 已經(jīng)夠用了.
jdk1.5原子包介紹(基于volatile)
包的特色:
1, 普通原子數(shù)值類(lèi)型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ù)組原子操作類(lèi)
AtomicIntegerArray, AtomicLongArray等等.
5, 大幅度提升系統(tǒng)吞吐量及性能.
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫(xiě)作最大的動(dòng)力,如果您喜歡我的文章,感覺(jué)我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
