ps: 老文章拆分出來的內(nèi)容
并發(fā)三原則
只有保證了這三點才能在并發(fā)中獲得想要的結果呕臂,我們想要進一步了解多線程朗儒,這3個原則必須要明白,并發(fā)都是圍繞這2個原則展開的
可見性 - 是指線程之間的可見性吮炕,一個線程修改的狀態(tài)對另一個線程是可見的腊脱。也就是一個線程修改的結果,另一個線程馬上就能看到来屠。簡單來說虑椎,線程的私有內(nèi)存中對象副本和主內(nèi)存對象的數(shù)據(jù)之間就是可見性問題,線程不把他私有內(nèi)存中的對象副本協(xié)會到主內(nèi)存中俱笛,那么對于其他想操作這個對象的線程來說就是"不可見的" 捆姜,最新數(shù)據(jù)不可見,所以此時其他線程可以獲取該對象的舊數(shù)據(jù)
原子性 - 對基本類型變量的讀取和賦值操作是原子性操作迎膜,即這些操作是不可中斷的泥技,要么執(zhí)行完畢,要么就不執(zhí)行磕仅。在多線程中珊豹,原子性是線程不安全的,其實和可見性有關聯(lián)榕订,線程在計算數(shù)據(jù)時會把在自己的工作區(qū)域 copy 一份數(shù)據(jù)的副本店茶,然后計算的是這個數(shù)據(jù)的副本,最后再把數(shù)據(jù)學會內(nèi)存中劫恒,這個過程里要是有別人線程同時操作同一個數(shù)據(jù)贩幻,那么我們的計算結果就是不正確了轿腺,就像打電話串線一樣,結果肯定不對
x =3;
y =4;
z = x+y;
x++丛楚;
上面第三行就包括了多個操作族壳,1是先讀取x的值,2讀取y的值趣些,3將計算中的值仿荆,4把z的值寫回內(nèi)存。一般一個語句含有多個操作該語句就不是原子性的操作坏平,只有最簡單的讀取和賦值才是原子性的操作
有序性 - 即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行拢操,上面講了因為原子性問題,絕部分操作都是可以再分的舶替,分成多個操作庐冯,這其中有對數(shù)據(jù)的讀,改坎穿,寫等展父,這些操作速度不一,JVM 為了提高效率玲昧,在保證結果相同的前提下栖茉,有計劃的多這么操作分組,在執(zhí)行需要等待的操作中孵延,穿插執(zhí)行其他執(zhí)行速度塊的操作吕漂,這叫指令重排序
指令重排序指的是在 保證程序最終執(zhí)行結果和代碼順序執(zhí)行的結果一致的前提下,改變語句執(zhí)行的順序來優(yōu)化輸入代碼尘应,提高程序運行效率惶凝。
重排序在單線程中沒啥問題,咱們等著執(zhí)行結果唄犬钢,反正重排序保證結果正確苍鲜。但是在多先撤我那個環(huán)境下是存在并發(fā)的,你這里對某一個對象的執(zhí)行重排序了玷犹,但是不是瞬間完成的混滔,這時另外的線程可以操作這個對象,那么你這個重排序后的執(zhí)行可能造成此時對象數(shù)據(jù)的不正確歹颓,會對其他線程使用這個對象產(chǎn)生影響
以下面的舉個例子:
int i = 0;
boolean flag = false;
i = 1; //語句1
flag = true; //語句2
定義了一個整形和Boolean型變量坯屿,并通過語句1和語句2對這兩個變量賦值,但是JVM在執(zhí)行這段代碼的時候并不保證語句1在語句2之前執(zhí)行巍扛,也就是說可能會發(fā)生 指令重排序领跛。
再來個例子:
//線程1:
context = loadContext(); //語句1
inited = true; //語句2
//線程2:
while (!inited) {
sleep()
}
doSomethingWithConfig(context);
對于線程1來說,語句1和語句2沒有依賴關系撤奸,因此有可能會發(fā)生指令重排序的情況吠昭。但是對于線程2來說鹅经,語句2在語句1之前執(zhí)行,那么就會導致進入doSomethingWithConfig函數(shù)的時候context沒有初始化
Java 提供了3個關鍵字 volatile怎诫、synchronized 和 final 來實現(xiàn)并發(fā)3原則
- final - 最好理解,一切都是不可變的贷痪,所以不在乎有多少個線程同時操作這個資源
- synchronized - 之前的文章介紹了幻妓,synchronized 保證了有序性,你想 synchronized 使用一把鎖鎖住了資源劫拢,那別人想用只能等著肉津,即便你再怎么重排序,我也能保證執(zhí)行效果
- volatile - 就比較復雜了舱沧,也是本文的重點
volatile
Volatile 是面試官最愛文的妹沙,即便你是做 android 開發(fā)的,你也逃不出去熟吏,所以大家好好鉆研吧
volatile 的特性: 先是非同步的 -> 保證了可見性 -> 同時也保證有序性 -> 但是不保證原子性
非同步 - volatile 修飾的變量不是 synchronized 的距糖,不是同步的,同一時間是能被多個線程操作的牵寺,所以 volatile 的使用范圍比較窄悍引,多用于修飾 static 靜態(tài)變量
保證可見性 - 好多地方都說 volatile 修飾的變量,線程直接和內(nèi)存交互帽氓,不會保存副本趣斤,而實際上線程還是會保存副本,只不過 CPU 每次都會從內(nèi)存中拿到最新的值黎休,并且改變數(shù)據(jù)之后立馬寫回內(nèi)存并通知其他改數(shù)據(jù)的備份數(shù)據(jù)改變了浓领,看上去就像線程直接和內(nèi)存交互一樣
不保證原子性 - volatile 語義并不能保證變量的原子性。對任意單個volatile變量的讀/寫具有原子性势腮,但類似于i++联贩、i–這種復合操作不具有原子性,因為自增運算包括讀取i的值捎拯、i值增加1撑蒜、重新賦值3步操作,并不具備原子性
-
保證有序性 - volatile 能夠屏蔽指令重排序:
- 當程序執(zhí)行到volatile變量的讀操作或者寫操作時玄渗,在其前面的操作的更改肯定全部已經(jīng)進行座菠,且結果已經(jīng)對后面的操作可見;在其后面的操作肯定還沒有進行藤树;
- 在進行指令優(yōu)化時浴滴,不能將在對volatile變量訪問的語句放在其后面執(zhí)行,也不能把volatile變量后面的語句放到其前面執(zhí)行岁钓。
volatile 適用場景:
- 禁止系統(tǒng)重排序的情況
- 只有一個線程寫升略,多個線程讀的情況
- 關鍵的標記行參數(shù)
- 靜態(tài)單例
當然了 volatile 最經(jīng)典的用處就是單例了
public class RxBus {
public static volatile RxBus instance;
public static RxBus getInstance() {
if (instance == null) {
synchronized (RxBus.class) {
if (instance == null) {
instance = new RxBus();
}
}
}
return instance;
}
}
我們對于靜態(tài)單例使用了 volatile 就能保證整個方法的執(zhí)行順序是按照我們縮寫的執(zhí)行微王。
若是我們不加 volatile ,在多線程時指令重排序品嚣,一個線程發(fā)現(xiàn) instance 是 null 的就會 new 一個對象出來炕倘,此時因為指令沖排序,很可能先在內(nèi)存 new 一塊空間然后賦值給 instance 翰撑,然后再去執(zhí)行實例化對象的操作罩旋,對象實例化的操作是比較重的。這是領一額個線程進來眶诈,發(fā)現(xiàn) instance 不是 null 涨醋,然后就去執(zhí)行代碼,但是此時 instance 實際只是有了一塊內(nèi)存地址逝撬,但是對象本身還沒初始化浴骂,就會產(chǎn)生空指針的問題
volatile 的優(yōu)勢是同步性能開銷比鎖低很多,若是使用 synchronized + 鎖宪潮,切換鎖給不同的線程要好幾毫秒溯警,比 new 個線程對象都耗費時間多了
但是 volatile 也有很嚴重的問題,那就是 volatile 不能保證原子性狡相,雖然 volatile 讓內(nèi)存可以同步到所有地方愧膀,但是并不能阻止多個線程同時操作同一個數(shù)據(jù),是沒法保證原子性的谣光,所以是不能代替 synchronized + 鎖檩淋,因此我們在使用 volatile 要及其小心,要思考會不會帶來并發(fā)問題萄金,一般我們見到的 volatile 應用都很少蟀悦,也都很死,都是固定的幾個場景使用