Java高并發(fā)--原子性可見性有序性
主要是學(xué)習(xí)慕課網(wǎng)實(shí)戰(zhàn)視頻《Java并發(fā)編程入門與高并發(fā)面試》的筆記
- 原子性:指一個(gè)操作不可中斷驶乾,一個(gè)線程一旦開始邑飒,直到執(zhí)行完成都不會(huì)被其他線程干擾。換句話說原子性保證了任何時(shí)刻只有一個(gè)線程在對共享變量進(jìn)行操作级乐。
- 可見性:指當(dāng)一個(gè)線程修改了某個(gè)共享變量的值疙咸,其他線程是否能立即知道這個(gè)修改。
- 有序性:一個(gè)線程觀察其他線程中的指令风科,由于指令重排序的存在撒轮,該觀察結(jié)果一般雜亂無序
原子性
AtomicInteger
JDK的atomic包下提供了許多“原子類”,它們都是基于CAS操作實(shí)現(xiàn)的贼穆。
所謂CAS(Compare And Swap)题山,即“比較并交換”。CAS基于樂觀的態(tài)度扮惦,是無鎖操作臀蛛,它操作包含三個(gè)參數(shù),當(dāng)前要更新的變量崖蜜、期望值浊仆、新值,僅當(dāng):當(dāng)前值和預(yù)期值一樣時(shí)豫领,才會(huì)將當(dāng)前值設(shè)置為新值抡柿;如果當(dāng)前值和預(yù)期值不一樣,說明這個(gè)變量已經(jīng)被其他線程修改過了等恐。如果有多個(gè)線程同時(shí)使用CAS操作一個(gè)變量時(shí)洲劣,只有一個(gè)會(huì)勝出备蚓,并成功更新。
以atomic包中最常用的AtomicInteger為例囱稽,追蹤其getAndIncrement()
郊尝,
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
可以看到它調(diào)用了unsafe.getAndAddInt(this, valueOffset, 1)
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
其中var1表示要被更新的對象,var2是原始值在內(nèi)存中的偏移地址战惊。通過getIntVolatile(var1, var2);
拿到現(xiàn)在的值var5流昏。但多個(gè)線程修改下,內(nèi)存中的原始值隨時(shí)都可能變化吞获,所以現(xiàn)在var5是一個(gè)期望值(期望內(nèi)存中的值和剛讀取到的var5是相等的况凉,因?yàn)閮?nèi)存中的值此時(shí)可能已經(jīng)變了)。compareAndSwapInt
是一個(gè)native方法各拷,compareAndSwapInt(var1, var2, var5, var5 + var4)
這句的意思是對于var1
對象刁绒,根據(jù)偏移地址var2
拿到的內(nèi)存中的原始值,如果和期望值var5
相等烤黍,則將其更新為var5 + var4
知市。同時(shí)從while-do也可以直到,該方法在一直嘗試速蕊,直到內(nèi)存中的值和期望值一樣時(shí)初狰,才能進(jìn)行修改,并返回修改前內(nèi)存中的值互例。
這里只是舉例解釋了其中一個(gè)方法奢入,其他方法的實(shí)現(xiàn)大同小異,總之都是使用了CAS操作來保證線程安全媳叨。
類似的還有AtomicLong腥光,AtomicBoolean,值得一提的還有有一個(gè)compareAndSet
方法糊秆,當(dāng)且僅當(dāng)期望值except和內(nèi)存中的值相等時(shí)武福,才會(huì)執(zhí)行更新操作。可以保證在多線程下同時(shí)修改共享變量痘番,只有一個(gè)線程可以修改成功捉片。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
舉個(gè)例子
package com.shy.concurrency.count;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author Haiyu
* @date 2018/12/17 17:33
*/
public class Test {
public static void main(String[] args) {
AtomicInteger a = new AtomicInteger(9);
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0;i < 10; i++) {
executorService.execute(()-> {
if (a.compareAndSet(9, 10)) {
System.out.println("更新成功");
System.out.println(a.get());
}
});
}
executorService.shutdown();
}
}
上面有十個(gè)線程要修改a
的值,但是最后程序只打印了一次10汞舱。因?yàn)橐坏┠硞€(gè)程序成功將a更新成10伍纫,其他線程的期望值except就和現(xiàn)在內(nèi)存中的值10不相等了,所以都會(huì)更新失敗昂芜。
LongAdder
像AtomicInteger等原子類使用CAS操作雖然沒有鎖莹规,但是也可以使用減小鎖粒度這種分離熱點(diǎn)的思想。LongAdder正是這樣的類泌神,它也位于atomic包下良漱,且有著比AtomicInteger等原子類更好的性能舞虱。LongAdder有一個(gè)稱為base的變量,如果在多線程下對base的修改沒有發(fā)生沖突的話母市,會(huì)直接操作base變量矾兜;但是如果發(fā)生了沖突,base這個(gè)熱點(diǎn)數(shù)據(jù)會(huì)被分離成多個(gè)單元cell患久,每個(gè)單元獨(dú)立維護(hù)內(nèi)部的值(通過哈希算法定位到數(shù)組中的某個(gè)cell)焕刮。這個(gè)對象的值其實(shí)是由cell數(shù)組的求和累加得到的,這樣熱點(diǎn)就進(jìn)行了有效的分離墙杯,提高了并行度。
AtomicReference
AtomicReference和AtomicInteger十分相似括荡,不過一個(gè)是對整數(shù)的封裝高镐,一個(gè)是對普通對象的封裝。
AtomicReference<Integer> money = new AtomicReference<>();
這樣寫就行了畸冲,泛型類型是Integer嫉髓,因此可以實(shí)現(xiàn)和AtomicInteger相同的功能。
AtomicStampedReference
CAS可能引發(fā)"ABA"問題邑闲,即一個(gè)變量原來是A算行,先被修改成B后又修改回了A,由于CAS操作只是比較當(dāng)前值和預(yù)期值是否一樣(只比較結(jié)果苫耸,不在乎過程中狀態(tài)的變化)州邢,在其他線程來看,該變量就好像沒有發(fā)生過變化褪子。
可以為數(shù)據(jù)添加時(shí)間戳量淌,每次成功修改數(shù)據(jù)時(shí),不僅更新數(shù)據(jù)的值嫌褪,同時(shí)要更新時(shí)間戳的值呀枢。CAS操作時(shí),不僅要比較當(dāng)前值和預(yù)期值笼痛,還要比較當(dāng)前時(shí)間戳和預(yù)期時(shí)間戳裙秋。兩者都必須滿足預(yù)期值才能修改成功。
AtomicStampedReference正是這樣做的缨伊,它不僅維護(hù)對象值摘刑,還維護(hù)了一個(gè)時(shí)間戳(其實(shí)可就是一個(gè)版本號)。當(dāng)AtomicStampedReference對應(yīng)的值被修改時(shí)刻坊,不僅要更新數(shù)據(jù)本身泣侮,還要更新時(shí)間戳(版本號)。只有當(dāng)數(shù)據(jù)本身和時(shí)間戳都滿足期望值紧唱,寫入才會(huì)成功活尊。因此隶校,雖然對象值被反復(fù)修改又被更新成了原來的值,但是時(shí)間戳發(fā)生了變化蛹锰,就可以防止不恰當(dāng)?shù)膶懭搿?/p>
AtomicIntegerFieldUpdater
該類可以使普通變量也擁有原子操作深胳。首先保證該普通變量是volatile的(且不能有static修飾符)。然后像下面這樣使用铜犬。
AtomicIntegerFieldUpdater<Money> updater = AtomicIntegerFieldUpdater.newUpdater(Money.class, "money");
其中Money類中有一個(gè)“money”的字段舞终,它的類型是普通的int型。通過上面的用法癣猾,使得Money中的money字段也擁有的原子性敛劝。
class Money {
volatile int money;
public int getMoney() {
return money;
}
}
運(yùn)行以下程序,將總得到打印值為10纷宇,說明這是線程安全的
package com.shy.concurrency.count;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.concurrent.atomic.AtomicReference;
/**
* @author Haiyu
* @date 2018/12/17 17:33
*/
@ThreadSafe
public class Test {
private volatile int count;
public int getCount() {
return count;
}
static AtomicIntegerFieldUpdater<Money> updater = AtomicIntegerFieldUpdater.newUpdater(Money.class, "money");
public static void main(String[] args) throws InterruptedException {
final Money money = new Money();
CountDownLatch cdl = new CountDownLatch(10);
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
executorService.execute(() -> {
updater.incrementAndGet(money);
cdl.countDown();
});
}
cdl.await();
System.out.println(money.getMoney());
executorService.shutdown();
}
}
AtomicIntegerArray
除了普通對象夸盟、基本數(shù)據(jù)類型的包裝類,atomic包還提供了原子數(shù)組像捶。如AtomicIntegerArray上陕,AtomicLongArray,在使用上無非就是加上了索引拓春。
比如
public final boolean compareAndSet(int i, int expect, int update) {...}
i
就是數(shù)組的索引释簿,其他API也類似,需要指定一個(gè)索引來明確表明要對哪一個(gè)變量進(jìn)行原子操作硼莽。
鎖
Java中有synchronized和重入鎖來保證線程的同步庶溶,以實(shí)現(xiàn)線程安全。
synchronized是JVM的內(nèi)置鎖懂鸵,而重入鎖是Java代碼實(shí)現(xiàn)的渐尿。重入鎖是synchronized的擴(kuò)展,可以完全代替后者矾瑰。重入鎖可以重入砖茸,允許同一個(gè)線程連續(xù)多次獲得同一把鎖。其次殴穴,重入鎖獨(dú)有的功能有:
- 可以相應(yīng)中斷凉夯,synchronized要么獲得鎖執(zhí)行,要么保持等待采幌。而重入鎖可以響應(yīng)中斷劲够,使得線程在遲遲得不到鎖的情況下,可以不再等待休傍。主要由
lockInterruptibly()
實(shí)現(xiàn)征绎,這是一個(gè)可以對中斷進(jìn)行響應(yīng)的鎖申請動(dòng)作,鎖中斷可以避免死鎖。 - 鎖的申請可以有等待時(shí)限人柿,用
tryLock()
可以實(shí)現(xiàn)限時(shí)等待柴墩,如果超時(shí)還未獲得鎖會(huì)返回false,也防止了線程遲遲得不到鎖時(shí)一直等待凫岖,可避免死鎖江咳。 - 公平鎖,即鎖的獲得按照線程先來后到的順序依次獲得哥放,不會(huì)產(chǎn)生饑餓現(xiàn)象歼指。synchronized的鎖默認(rèn)是不公平的,重入鎖可通過傳入構(gòu)造方法的參數(shù)實(shí)現(xiàn)公平鎖甥雕。
- 重入鎖可以綁定多個(gè)Condition條件踩身,這些condition通過調(diào)用await/singal實(shí)現(xiàn)線程間通信。
synchronized可以作用在如下四個(gè)地方:
- 代碼塊社露,使用當(dāng)前對象或其他任意對象作為鎖挟阻。被代碼塊包圍的代碼會(huì)同步執(zhí)行。
synchronized(this)
和synchronized(obj)
就分別使用自身和obj對象作為鎖呵哨。 - 修飾方法,使用當(dāng)前對象作為鎖轨奄。整個(gè)方法會(huì)同步執(zhí)行
- 修飾靜態(tài)方法孟害,使用類作為鎖(因此作用于該類的所有對象)。整個(gè)方法會(huì)同步執(zhí)行
注意在多線程下如果要保證synchronized的線程安全挪拟,必須使用同一把鎖挨务。
可見性
導(dǎo)致可見性的原因:
- 線程交叉執(zhí)行
- 指令重排結(jié)合線程交叉執(zhí)行
- 共享變量更新后沒有在工作內(nèi)存和主內(nèi)存之間及時(shí)更新
synchronized的可見性
- 線程解鎖前,必須將共享變量的最新值刷回主內(nèi)存中玉组。
- 線程加鎖時(shí)谎柄,將清空工作內(nèi)存中共享變量的值,從而使用共享變量時(shí)需要從主內(nèi)存中重新讀取最新的值(注意加鎖與解鎖使用同一個(gè)鎖)惯雳。
volatile的可見性
- 對volatile變量的寫操作朝巫,會(huì)在寫操作后加入一條store屏障指令,將工作內(nèi)存中的共享變量值刷新到主內(nèi)存中石景;
- 對volatile變量的讀操作劈猿,會(huì)在讀操作前加入一條load屏障指令,讀主內(nèi)存中讀取共享變量
換句話說潮孽,volatile的作用是:在本CPU對變量的修改直接寫入主內(nèi)存中揪荣,同時(shí)這個(gè)寫操作使得其他CPU中對應(yīng)變量的緩存行無效,這樣其他線程在讀取這個(gè)變量時(shí)候必須從主內(nèi)存中讀取往史,所以讀取到的是最新的仗颈,這就是上面說得能被立即“看到”。
volatile常使用于標(biāo)志位的判斷椎例。
volatile boolean ready= false;
// 線程1
config = loadConfig(); // 語句1
ready = true; // 語句2
// 線程2,ready為true時(shí)才停止sleep()
while (!ready) {
sleep();
}
runWithConfig(config);
如上面的例子挨决,如果ready變量不是volatile的请祖,有可能因?yàn)橹噶钪嘏牛葓?zhí)行語句2再執(zhí)行語句1凰棉。由于先執(zhí)行語句2损拢,那么線程2中再config還沒有初始化時(shí)就執(zhí)行了runWithConfig(config),這顯然是不合理的撒犀。當(dāng)ready加上了volatile修飾符福压,禁止了指令重排,因此不會(huì)發(fā)生以上情況或舞。
有序性
Happen-Before規(guī)則
有些指令是可以重排的荆姆,有些指令是不可重排的。下面是一些基本原則:
- 程序順序原則:一個(gè)線程內(nèi)保證語義的串行性映凳,比如第二條語句依賴第一條語句的結(jié)果胆筒,那么就不能先執(zhí)行第二條再執(zhí)行第一條。
- volatile原則:volatile變量的寫先于讀诈豌,著保證了volatile變量的可見性
- 鎖規(guī)則:先解鎖仆救,后續(xù)步驟再加鎖。加鎖不能重排到解鎖之前矫渔,這樣加鎖行為無法獲得鎖(剛加上就解了)
- 傳遞性:A先于B彤蔽,B先于C,那么A先于C
- 線程的
start()
先于它的每個(gè)動(dòng)作 - 線程的所有操作先于線程的終結(jié)(可以通過
Tread.join()
方法結(jié)束庙洼、Thread.isAlive()
的返回值判斷一個(gè)線程是否終結(jié)) - 線程的中斷(
interrupt()
)先于被中斷線程的代碼 - 對象的構(gòu)造函數(shù)執(zhí)行顿痪、結(jié)束先于
finalize()
方法。