volatile, 用更低的代價替代同步
為什么
使用volatile比同步代價更低?
同步的代價, 主要由其覆蓋范圍決定, 如果可以降低同步的覆蓋范圍, 則可以大幅提升程序性能.?
而volatile的覆蓋范圍僅僅變量級別的. 因此它的同步代價很低.
volatile原理是什么?
volatile的語義, 其實是告訴處理器, 不要將我放入工作內存, 請直接在主存操作我.(工作內存詳見java內存模型)
因此, 當多核或多線程在訪問該變量時, 都將直接
操作
主存, 這從本質上, 做到了變量共享.
volatile的有什么優勢?
1, 更大的程序吞吐量
2, 更少的代碼實現多線程
3, 程序的伸縮性較好
4, 比較好理解, 無需太高的學習成本
volatile有什么劣勢?
1, 容易出問題
2, 比較難設計
?
?
在java線程并發處理中,有一個關鍵字volatile的使用目前存在很大的混淆,以為使用這個關鍵字,在進行多線程并發處理的時候就可以萬事大吉。
?
Java語言是支持多線程的,為了解決線程并發的問題,在語言內部引入了 同步塊 和 volatile 關鍵字機制。
?
?
?
synchronized ?
?
同步塊大家都比較熟悉,通過 synchronized 關鍵字來實現,所有加上synchronized 和 塊語句,在多線程訪問的時候,同一時刻只能有一個線程能夠用
?
synchronized 修飾的方法 或者 代碼塊。
?
volatile
?
用volatile修飾的變量,線程在每次使用變量的時候,都會讀取變量修改后的最的值。volatile很容易被誤用,用來進行原子性操作。
?
下面看一個例子,我們實現一個計數器,每次線程啟動的時候,會調用計數器inc方法,對計數器進行加一
?執行環境——jdk版本:jdk1.6.0_31 ,內存 :3G?? cpu:x86 2.4G
運行結果:Counter.count=995
?
?實際運算結果每次可能都不一樣,本機的結果為:運行結果:Counter.count=995,可以看出,在多線程的環境下,Counter.count并沒有期望結果是1000
?
?
很多人以為,這個是多線程并發問題,只需要在變量count之前加上volatile就可以避免這個問題,那我們在修改代碼看看,看看結果是不是符合我們的期望
?
運行結果:Counter.count=992
運行結果還是沒有我們期望的1000,下面我們分析一下原因
?
?
?
在 java 垃圾回收整理一文中,描述了jvm運行時刻內存的分配。其中有一個內存區域是jvm虛擬機棧,每一個線程運行時都有一個線程棧,
?
線程棧保存了線程運行時候變量值信息。當線程訪問某一個對象時候值的時候,首先通過對象的引用找到對應在堆內存的變量的值,然后把堆內存
?
變量的具體值load到線程本地內存中,建立一個變量副本,之后線程就不再和對象在堆內存變量值有任何關系,而是直接修改副本變量的值,
?
在修改完之后的某一個時刻(線程退出之前),自動把線程變量副本的值回寫到對象在堆中變量。這樣在堆中的對象的值就產生變化了。?
?
read and load 從主存復制變量到當前工作內存
use and assign? 執行代碼,改變共享變量值
?
store and write 用工作內存數據刷新主存相關內容
?
其中use and assign 可以多次出現
?
但是這一些操作并不是原子性,也就是 在read load之后,如果主內存count變量發生修改之后,線程工作內存中的值由于已經加載,不會產生對應的變化,所以計算出來的結果會和預期不一樣
?
對于volatile修飾的變量,jvm虛擬機只是保證從主內存加載到線程工作內存的值是最新的
?
例如假如線程1,線程2 在進行read,load 操作中,發現主內存中count的值都是5,那么都會加載這個最新的值
?
在線程1堆count進行修改之后,會write到主內存中,主內存中的count變量就會變為6
?
線程2由于已經進行read,load操作,在進行運算之后,也會更新主內存count的變量值為6
?
導致兩個線程及時用volatile關鍵字修改之后,還是會存在并發的情況。
?
?
如何避免這種情況?
解決以上問題的方法:
一種是 操作時, 加上同步.
這種方法, 無疑將大大降低程序性能, 且違背了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類分析:)
?
?
???
這個方法是, 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, 大幅度提升系統吞吐量及性能.
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

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