Java程序員必知的并發(fā)編程藝術(shù)——并發(fā)機制的底層原理實現(xiàn)

Java編程語言允許線程訪問共享變量,為了確保共享變量能被準確和一致的更新,線程應該確保通過排他鎖單獨獲得這個變量。

volatile借助Java內(nèi)存模型保證所有線程能夠看到最新的值。(內(nèi)存可見性)

實現(xiàn)原理:

將帶有volatile變量操作的Java代碼轉(zhuǎn)換成匯編代碼后,可以看到多了個lock前綴指令(X86平臺CPU指令)。這個lock指令是關鍵,在多核處理器下實現(xiàn)兩個重要操作:

1.將當前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存。

2.這個寫回內(nèi)存的操作會使其他處理器里緩存該內(nèi)存地址的數(shù)據(jù)失效

如果了解計算機組成原理,可以知道CPU為了提高處理速度,不和內(nèi)存直接進行交互,而是使用Cache(高速緩存,通過緩存數(shù)據(jù)交互速度和內(nèi)存不是一個數(shù)量級,而同時Cache的存儲容量也很小)。

從內(nèi)存將數(shù)據(jù)讀到緩存后,CPU進行一系列數(shù)據(jù)操作,而操作完成時間是不可知的。而JVM對帶有volatile變量進行寫操作時,會發(fā)送Lock前綴指令,將數(shù)據(jù)從緩存行寫入到內(nèi)存。寫入內(nèi)存還不夠,因為其他線程的緩存行中數(shù)據(jù)還是舊的,Lock指令可以讓其他CPU通過監(jiān)聽在總線上的數(shù)據(jù),檢查自己的緩存數(shù)據(jù)是否過期,如果緩存行的地址和總線上的地址相同,則將緩存行失效,下次該線程對這個數(shù)據(jù)操作時,會重新從內(nèi)存中讀取,更新到緩存行。

2.Synchronized

Synchronized也是經(jīng)常用到的,它給人的印象一般是”重量級鎖”。在JDK1.6后,對Synchronized進行了一系列優(yōu)化,引入了偏向鎖和輕量級鎖,對鎖的存儲結(jié)構(gòu)和升級過程。有效減少獲得鎖和釋放鎖帶來的性能消耗。

Synchronized同步基礎:

1.普通同步方法,鎖是當前實例對象。 public synchronized void test(){…}

2.靜態(tài)同步方法,鎖是當前類的Class對象。public static synchronized void test(…){}

3.對于同步方法塊,鎖是Synchronized括號中里配置的對象。synchronized(instance){…}

用javap反編譯class文件,可以看到Synchronized用的是monitorenter和monitorexit實現(xiàn)加鎖。一個monitorenter必須要有monitorexit與之對應,所以同步方法會在異常處和方法返回處加入monitorexit指令。

3: monitorenter //注意此處,進入同步方法 4: aload_0 5: dup 6: getfield #2 // Field i:I 9: iconst_110: iadd11: putfield #2 // Field i:I14: aload_115: monitorexit //注意此處,退出同步方法

3.Java對象頭

Synchronized用到的鎖存在Java對象頭里,若對象非數(shù)組類型,用32bit存儲(2個字寬,32虛擬機一個字寬為4字節(jié),一個字節(jié)8bit)

MarkWord存儲和鎖相關的信息:

鎖有四個等級: 無鎖->偏向鎖->輕量級鎖->重量級鎖。如果存在競爭,就會不斷升級,但不會降級。

1.偏向鎖

多數(shù)情況下,鎖不會存在競爭,而是同一個線程多次獲得。當某個線程訪問同步塊代碼時,會將鎖對象和棧幀中的鎖記里存儲鎖偏向的線程ID,以后線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單比對一下對象頭中的MarkWord里的線程ID,如果一致則表示線程獲得鎖。若不一致,再繼續(xù)測試偏向鎖的標識是否為1:如果沒有設置(無鎖狀態(tài)),用CAS(Compare and Swap)競爭鎖;如果設置了,嘗試使用CAS將對象頭的偏向鎖指向當前線程。

當有另一個線程嘗試競爭鎖時,持有偏向鎖的線程才會釋放鎖。需要等待全局安全點(在這個時間點上沒有字節(jié)碼正在執(zhí)行),它會首先暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否活著,如果線程不處于活動狀態(tài),則將對象頭設置成無鎖狀態(tài),如果線程仍然活著,擁有偏向鎖的棧會被執(zhí)行,遍歷偏向?qū)ο蟮逆i記錄,棧中的鎖記錄和對象頭的Mark Word,要么重新偏向于其他線程,要么恢復到無鎖或者標記對象不適合作為偏向鎖,最后喚醒暫停的線程。

Java 6,7默認開啟偏向鎖,可以通過JVM的參數(shù)-XX:-UsebiasedLocking=false關閉

2.輕量級鎖

(1)加鎖

鎖記錄存儲在棧楨,會將對象頭的MarkWord復制到鎖記錄。線程在執(zhí)行同步塊時,會嘗試用CAS將對象頭的MarkWord替換為指向鎖記錄的指針,若成功,獲得鎖;失敗表示其他線程競爭鎖,當前線程嘗試使用自旋獲取鎖。

(2)解鎖

類似于加鎖反向操作,會將鎖記錄復制會對象頭的MarkWord。若成功,表示操作過程中沒有競爭發(fā)生;若失敗,存在競爭,鎖會膨脹成重量級鎖。

如下圖:

當膨脹到重量級鎖時,不會再通過自選獲得鎖(自旋時線程處于活動狀態(tài),會消耗CPU),而是將線程阻塞,獲得鎖的線程執(zhí)行完后會釋放重量級鎖,此時喚醒因為鎖阻塞的線程,進行新一輪的競爭。

3.其他鎖概念

自旋鎖:

自旋鎖是采用讓當前線程不停地的在循環(huán)體內(nèi)執(zhí)行實現(xiàn)的,當循環(huán)的條件被其他線程改變時 才能進入臨界區(qū)。

public class SpinLock { private AtomicReference sign =new AtomicReference<>(); public void lock(){

Thread current = Thread.currentThread(); while(!sign .compareAndSet(null, current)){

}

} public void unlock (){

Thread current = Thread.currentThread();

sign .compareAndSet(current, null);

}

}

使用了CAS原子操作,lock函數(shù)將owner設置為當前線程,并且預測原來的值為空。unlock函數(shù)將owner設置為null,并且預測值為當前線程。

當有第二個線程調(diào)用lock操作時由于owner值不為空,導致循環(huán)一直被執(zhí)行,直至第一個線程調(diào)用unlock函數(shù)將owner設置為null,第二個線程才能進入臨界區(qū)。

由于自旋鎖只是將當前線程不停地執(zhí)行循環(huán)體,不進行線程狀態(tài)的改變,所以響應速度更快。但當線程數(shù)不停增加時,性能下降明顯,因為每個線程都需要執(zhí)行,占用CPU時間。如果線程競爭不激烈,并且保持鎖的時間段。適合使用自旋鎖。

鎖的優(yōu)缺點對比:

優(yōu)點缺點適用場景偏向鎖加鎖和解鎖不需要額外的消耗,和執(zhí)行非同步方法比僅存在納秒級的差距如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗適用于只有一個線程訪問同步塊場景輕量級鎖競爭的線程不會阻塞,提高了程序的響應速度如果始終得不到鎖競爭的線程使用自旋會消耗CPU追求響應時間,鎖占用時間很短重量級鎖線程競爭不使用自旋,不會消耗CPU線程阻塞,響應時間緩慢追求吞吐量,鎖占用時間較長

想透徹理解多線程并發(fā)編程可以參考下面的思維導圖:獲取免費的架構(gòu)學習資料可以加群:777158424?.里面有分布式,微服務,性能優(yōu)化,spring mybatis tomcat源碼分析等資料免費分享。還有下圖的多線程并發(fā)學習資源哦~全部免費可以獲取。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容