目錄
- 并行與并發(fā)區(qū)別
- Java中的線程安全問題
- Java中共享變量的內(nèi)存可見性問題
- synchronized關(guān)鍵字
- volatile關(guān)鍵字
- Java中的CAS操作
- Unsafe類
- Java指令重排序
- 鎖
- 更多
并行與并發(fā)區(qū)別
并發(fā)指同一時(shí)間段多個(gè)任務(wù)同時(shí)都在進(jìn)行桥嗤,并且都沒有執(zhí)行結(jié)束泛啸,而并行是說在單位時(shí)間內(nèi)多個(gè)任務(wù)在同時(shí)運(yùn)行。
并發(fā)任務(wù)強(qiáng)調(diào)在一個(gè)時(shí)間段內(nèi)同時(shí)進(jìn)行,而一個(gè)時(shí)間段有多個(gè)單位i時(shí)間構(gòu)成,所以說并發(fā)的多個(gè)任務(wù)在單位時(shí)間內(nèi)不一定同時(shí)在執(zhí)行。
一個(gè)CPU同時(shí)只能執(zhí)行一個(gè)任務(wù)熬北,所以單CPU時(shí)代多個(gè)任務(wù)都是并發(fā)執(zhí)行的。
注:在多線程時(shí)間中诚隙,線程的個(gè)數(shù)往往多于CPU個(gè)數(shù)讶隐,所以即使存在并行任務(wù),一般還是稱為多線程并發(fā)編程而非多線程并行編程久又。
Java中的線程安全問題
示例:計(jì)數(shù)器問題
t1 | t2 | t3 | t4 | |
---|---|---|---|---|
線程A | 從主內(nèi)存讀取count值到本線程 | 遞增本地線程count的值 | 寫回主內(nèi)存 | |
線程B | 從主內(nèi)存讀取count值到本線程 | 遞增本地線程count的值 | 寫回主內(nèi)存 |
假設(shè)count初始值為0巫延,線程A在t1和t2時(shí)間讀取了主內(nèi)存中的count并在本地將其遞增為1,t2時(shí)線程B從主內(nèi)存中讀取了count的值0并于t3時(shí)將其遞增為1地消,t3時(shí)線程A將count的新值1更新到主內(nèi)存烈评,t4時(shí)線程B進(jìn)行了同樣的操作,最終主內(nèi)存中count的值為1而非我們想要的2犯建。
Java中共享變量的內(nèi)存可見性問題
如圖是一個(gè)雙核CPU模型讲冠,每個(gè)核都有自己的一級緩存,有些架構(gòu)里還有一個(gè)所有CPU共享的二級緩存适瓦。
現(xiàn)假設(shè)線程A和線程B同時(shí)處理一個(gè)共享變量竿开,由于Cache的存在谱仪,將會(huì)出現(xiàn)內(nèi)存不可見問題,原因如下:
- 線程A先獲取共享變量X的值(假設(shè)X=0)否彩,由于L1和L2緩存中都沒有X的值疯攒,線程A會(huì)直接從主存中去取X的值并將其緩存到L1和L2中。然后A將X的值遞增為1列荔,將其寫入緩存L1和L2敬尺,并刷新到主存。
- 線程B也要獲取X的值贴浙,由于Core2中1級緩存沒有X的值砂吞,B會(huì)從L2緩存取到X的值1。然后B修改X為2崎溃,將其更新至L1蜻直、L2和主存。
- 線程A又需要獲取X的值袁串,發(fā)現(xiàn)L1中已經(jīng)有了概而,但此時(shí)A獲得的X=1,與主存中X=2不同了囱修!
synchronized關(guān)鍵字
synchronized關(guān)鍵字是一種原子性內(nèi)置鎖赎瑰,線程進(jìn)入synchronized代碼塊前會(huì)自動(dòng)獲取監(jiān)視器鎖,這時(shí)其他線程再訪問該同步代碼塊是會(huì)被阻塞掛起破镰。
前面的共享變量內(nèi)存可見性問題主要是線程的工作內(nèi)存導(dǎo)致的乡范,而synchronized的內(nèi)存語義可以解決此問題。
synchronized的內(nèi)存語義:
- 進(jìn)入synchronized塊:把synchronized塊中使用到的變量從線程的工作內(nèi)存中清除啤咽,這樣當(dāng)synchronized塊中要使用該變量時(shí)晋辆,就會(huì)從主存中去取。
- 離開synchronized塊:把synchronized塊中對共享變量的修改刷新到主內(nèi)存宇整。
示例
public class ThreadSafeInteger {
private int value;
public synchronized int get() {
return value;
}
public synchronized void set(int value) {
this.value = value;
}
}
注1:get()方法雖然只是讀操作瓶佳,但仍要加上synchronized來實(shí)現(xiàn)value的內(nèi)存可見性。
注2:使用synchronized雖然解決了共享變量value的內(nèi)存可見性問題鳞青,但由于synchronized是獨(dú)占鎖霸饲,同時(shí)只能有一個(gè)線程調(diào)用get()方法,其他調(diào)用線程則會(huì)被阻塞臂拓,同時(shí)存在線程切換厚脉、調(diào)度的開銷,效率并不高胶惰。
volatile關(guān)鍵字
當(dāng)一個(gè)變量聲明為volatile時(shí)傻工,線程在寫入變量時(shí)就不會(huì)把值緩存,而是直接把值刷新到主存中;當(dāng)其他線程讀取該變量時(shí)中捆,會(huì)從主存中重新獲得最新值鸯匹,而不是使用當(dāng)前工作內(nèi)存中的值。
示例
public class ThreadSafeInteger {
private volatile int value;
public int get() {
return value;
}
public void set(int value) {
this.value = value;
}
}
注:volatile雖然保證了可見性泄伪,但并不保證操作的原子性殴蓬。
volatile不保證原子性示例
public class Test {
private static volatile long _longVal = 0;
public static void main(String[] args) {
Thread t1 = new Thread(new LoopVolatile1());
t1.start();
Thread t2 = new Thread(new LoopVolatile2());
t2.start();
try{
t1.join();
t2.join();
}catch(Exception e){
e.printStackTrace();
}
System.out.println("final val is: " + _longVal);
}
private static class LoopVolatile1 implements Runnable {
public void run() {
long val = 0;
while (val < 100000) {
_longVal++;
val++;
}
}
}
private static class LoopVolatile2 implements Runnable {
public void run() {
long val = 0;
while (val < 100000) {
_longVal++;
val++;
}
}
}
}
運(yùn)行上述代碼,發(fā)現(xiàn)每次輸出不同蟋滴。
使用場景
- 寫入變量值不依賴當(dāng)前值染厅。如果依賴當(dāng)前值,將是獲取-計(jì)算-寫入三步津函,這三步并非原子性肖粮,volatile也不保證原子性。
- 讀寫變量值時(shí)沒有加鎖球散。因?yàn)榧渔i本身已經(jīng)保證了內(nèi)存可見性,再使用volatile就是畫蛇添足散庶。
Java中的CAS操作
CAS即Compare And Swap蕉堰,JDK中Unsafe類提供了一系列的compareAndSwap*方法。
示例
下面以compareAndSwapLong(Object obj, long valueOffset, long expect, long update)方法為例進(jìn)行介紹
boolean compareAndSwapLong(Object obj, long valueOffset, long expect, long update)方法:如果obj對象中內(nèi)存偏移值為valueOffset的變量值為expect悲龟,則使用新的值update替換之屋讶。這是處理器提供的一個(gè)原子性指令。
ABA問題
問題描述
假如線程A要去通過CAS修改變量X须教,要先判斷X當(dāng)前值是否改變過皿渗,如果“未改變”,則更新之轻腺。但這并不能保證X沒有被改變過:假如A修改X前乐疆,線程B修改了X的值,然后又修改回來贬养,A的CAS操作仍能成功挤土,但X實(shí)際上發(fā)生過改變。
解決方案
JDK中的AtomicStampedReference類給每個(gè)變量都配備了一個(gè)時(shí)間戳误算,從而避免了ABA問題的產(chǎn)生仰美。
Unsafe類
JDK的rt.jar 包中的UnSafe類提供了硬件級別的原子性操作,Unsafe類中的方法都是native方法儿礼,使用JNI的方式訪問本地C++實(shí)現(xiàn)庫咖杂。
Unsafe類可以直接操作內(nèi)存,這是不安全的蚊夫,如果是普通的調(diào)用的話诉字,它會(huì)拋出一個(gè)SecurityException異常;只有由主類加載器加載的類才能調(diào)用這個(gè)方法。
見下:
public static Unsafe getUnsafe() {
Class localClass = Reflection.getCallerClass();
if(!VM.isSystemDomainLoader(localClass.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
但通過萬能的反射奏窑,還是可以使用到Unsafe類的:
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
Java指令重排序
Java內(nèi)存模型允許編譯器和處理器對不存在數(shù)據(jù)依賴性的指令進(jìn)行重排序以提高性能导披。
如下:
int a = 1; //(1)
int b = 2; //(2)
int c = a + b; //(3)
如果有必要,JVM完全可以將(2)放在(1)前執(zhí)行埃唯,但這并不會(huì)影響最終結(jié)果撩匕。
單線程下這樣做是ok的,但多線程下就會(huì)出現(xiàn)問題:
private static int num = 0;
private static boolean ready = false;
public static void main(String[] args){
Thread t1 = new Thread(new ReadTask());
Thread t2 = new Thread(new WriteTask());
t1.start();
t2.start();
try{
Thread.sleep(100);
}catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("main exit!");
}
public static class ReadTask implements Runnable {
@Override
public void run() {
while(true) {
if(ready) {
System.out.println(num);
return;
}
}
}
}
public static class WriteTask implements Runnable {
@Override
public void run() {
num = 1; //(1)
ready = true; //(2)
}
}
理論上這段代碼并不一定輸出1墨叛,因?yàn)檫M(jìn)行指令重排序后止毕,WriteTask中的(2)語句有可能會(huì)先于(1)執(zhí)行,導(dǎo)致輸出為0漠趁。
鎖
樂觀鎖與悲觀鎖
- 樂觀鎖:認(rèn)為數(shù)據(jù)在一般情況下不會(huì)造成沖突扁凛,所以在訪問記錄前不會(huì)加排他鎖,而是在進(jìn)行數(shù)據(jù)提交更新時(shí)才會(huì)對數(shù)據(jù)沖突進(jìn)行檢測并返回一定狀態(tài)闯传。用戶需根據(jù)狀態(tài)判斷是否更新成功并進(jìn)行相應(yīng)操作谨朝。
- 悲觀鎖:對數(shù)據(jù)被外界修改持保守態(tài)度,認(rèn)為數(shù)據(jù)很容易被其他線程更改甥绿,所以在數(shù)據(jù)被處理前進(jìn)行加鎖字币。
公平鎖與非公平鎖
- 公平鎖:線程獲取鎖的順序是按照請求鎖的時(shí)間順序決定的,先請求先得共缕。
- 非公平鎖:不保證先請求的線程先獲得鎖洗出。
注:在沒有公平性需求的前提下盡量使用非公平鎖,因?yàn)楣芥i會(huì)帶來額外性能開銷图谷。
獨(dú)占鎖與共享鎖
- 獨(dú)占鎖:任何時(shí)候都只有一個(gè)線程能得到鎖翩活,如ReentrantLock。
- 共享鎖:可以多個(gè)線程持有便贵,如ReadWriteLock讀寫鎖菠镇。
可重入鎖
一個(gè)線程要再次獲取自己已經(jīng)獲取了的鎖時(shí)如果不被阻塞,則該鎖為可重入鎖承璃。
public class Test{
public synchronized void f1() {
System.out.println("f1...");
}
public synchronized void f2() {
System.out.println("f2...");
f1();
}
}
上述代碼中辟犀,調(diào)用f2()并不會(huì)造成阻塞,說明synchronized內(nèi)部鎖是可重入鎖绸硕。
自旋鎖
當(dāng)前線程獲取鎖時(shí)堂竟,如果發(fā)現(xiàn)鎖已經(jīng)被其他線程占有,并不會(huì)馬上阻塞自己玻佩,而是在不放棄CPU使用權(quán)的情況下出嘹,多次嘗試獲取,到一定次數(shù)后才放棄咬崔。目的是使用CPU時(shí)間換取線程阻塞與調(diào)度的開銷税稼。
更多
相關(guān)筆記:《Java并發(fā)編程之美》閱讀筆記