Java核心(三)并发中的线程同步与锁
2019獨(dú)角獸企業(yè)重金招聘Python工程師標(biāo)準(zhǔn)>>>
樂觀鎖、悲觀鎖、公平鎖、自旋鎖、偏向鎖、輕量級鎖、重量級鎖、鎖膨脹...難理解?不存的!來,話不多說,帶你飆車。
上一篇介紹了線程池的使用,在享受線程池帶給我們的性能優(yōu)勢之外,似乎也帶來了另一個問題:線程安全的問題。
那什么是線程的安全問題呢?
一、線程安全問題的產(chǎn)生
線程安全問題:指的是在多線程編程中,同時操作同一個可變的資源之后,造成的實際結(jié)果與預(yù)期結(jié)果不一致的問題。
比如:A和B同時向C轉(zhuǎn)賬10萬元。如果轉(zhuǎn)賬操作不具有原子性,A在向C轉(zhuǎn)賬時,讀取了C的余額為20萬,然后加上轉(zhuǎn)賬的10萬,計算出此時應(yīng)該有30萬,但還未來及將30萬寫回C的賬戶,此時B的轉(zhuǎn)賬請求過來了,B發(fā)現(xiàn)C的余額為20萬,然后將其加10萬并寫回。然后A的轉(zhuǎn)賬操作繼續(xù)——將30萬寫回C的余額。這種情況下C的最終余額為30萬,而非預(yù)期的40萬。
如果上面的內(nèi)容您還沒有理解,沒關(guān)系,我們來看下面非安全線程的模擬代碼:
public class ThreadSafeSample {public int number;public void add() {for (int i = 0; i < 100000; i++) {int former = number++;int latter = number;if (former != latter-1){System.out.printf("非相等 former=" + former + " latter=" + latter);}}}public static void main(String[] args) throws InterruptedException {ThreadSafeSample threadSafeSample = new ThreadSafeSample();Thread threadA = new Thread(new Runnable() {@Overridepublic void run() {threadSafeSample.add();}});Thread threadB = new Thread(new Runnable() {@Overridepublic void run() {threadSafeSample.add();}});threadA.start();threadB.start();threadA.join();threadB.join();} }我電腦運(yùn)行的結(jié)果: 非相等 => former=5555 latter=6061
可以看到,僅僅是兩個線程的低度并發(fā),就非常容易碰到 former 和 latter 不相等的情況。這是因為,在兩次取值的過程中,其他線程可能已經(jīng)修改了number.
二、線程安全的解決方案
線程安全的解決方案分為以下幾個維度(參考《碼出高效:Java開發(fā)手冊》):
- 數(shù)據(jù)單線程可見(單線程操作自己的數(shù)據(jù)是不存在線程安全問題的,ThreadLocal就是采用這種解決方案);
- 數(shù)據(jù)只讀;
- 使用線程安全類(比如StringBuffer就是一個線程安全類,內(nèi)部是使用synchronized實現(xiàn)的);
- 同步與鎖機(jī)制;
解決線程安全核心思想是:“要么只讀,要么加鎖”,解決線程安全的關(guān)鍵在于合理的使用Java提供的線程安全包java.util.concurrent簡稱JUC。
三、線程同步與鎖
Java 5 以前,synchronized是僅有的同步手段,Java 5的時候增加了ReentrantLock(再入鎖)它的語義和synchronized基本相同,比synchronized更加靈活,可以做到更多的細(xì)節(jié)控制,比如鎖的公平性/非公平性指定。
3.1 synchronized
synchronized 是 Java 內(nèi)置的同步機(jī)制,它提供了互斥的語義和可見性,當(dāng)一個線程已經(jīng)獲取當(dāng)前鎖時,其他試圖獲取的線程只能等待或者阻塞在那里。
3.1.1 synchronized 使用
synchronized 可以用來修飾方法和代碼塊。
3.1.1.1 修飾代碼塊
synchronized (this) {int former = number++;int latter = number;//... }3.1.1.2 修飾方法
public synchronized void add() {//... }3.1.2 synchronized 底層實現(xiàn)原理
synchronized 是由一對 monitorenter/monitorexit 指令實現(xiàn)的,Monitor 對象是同步的基本實現(xiàn)單元。在 Java 6 之前,Monitor的實現(xiàn)完全是依靠操作系統(tǒng)內(nèi)部的互斥鎖,因為需要進(jìn)行用戶態(tài)到內(nèi)核態(tài)的切換,所以同步操作是一個無差別的重量級操作,性能也很低。但在Java 6的時候,JVM 對此進(jìn)行了大刀闊斧地改進(jìn),提供了三種不同的 Monitor 實現(xiàn),也就是常說的三種不同的鎖:偏向鎖(Biased Locking)、輕量級鎖和重量級鎖,大大改進(jìn)了其性能。
3.1.2.1 偏向鎖/輕量級鎖/重量級鎖
偏向鎖是為了解決在沒有多線程的訪問下,盡量減少鎖帶來的性能開銷。
輕量級鎖是指當(dāng)鎖是偏向鎖的時候,被另一個線程所訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,提高性能。
重量級鎖是指當(dāng)鎖為輕量級鎖的時候,另一個線程雖然是自旋,但自旋不會一直持續(xù)下去,當(dāng)自旋一定次數(shù)的時候,還沒有獲取到鎖,就會進(jìn)入阻塞,該鎖膨脹為重量級鎖。重量級鎖會讓其他申請的線程進(jìn)入阻塞,性能降低。
3.1.2.2 鎖膨脹(升級)原理
Java 6 之后優(yōu)化了 synchronized 實現(xiàn)方式,使用了偏向鎖升級為輕量級鎖再升級到重量級鎖的方式,減低了鎖帶來的性能消耗,也就是我們常說的鎖膨脹或者叫鎖升級,那么它是怎么實現(xiàn)鎖升級的呢?
鎖膨脹(升級)原理: 在鎖對象的對象頭里面有一個ThreadId字段,在第一次訪問的時候ThreadId為空,JVM讓其持有偏向鎖,并將ThreadId設(shè)置為其線程id,再次進(jìn)入的時候會先判斷ThreadId是否尤其線程id一致,如果一致則可以直接使用,如果不一致,則升級偏向鎖為輕量級鎖,通過自旋循環(huán)一定次數(shù)來獲取鎖,不會堵塞,執(zhí)行一定次數(shù)之后就會升級為重量級鎖,進(jìn)入堵塞,整個過程就是鎖膨脹(升級)的過程。
3.1.2.3 自旋鎖
自旋鎖是指嘗試獲取鎖的線程不會立即阻塞,而是采用循環(huán)的方式去嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗,缺點(diǎn)是循環(huán)會消耗CPU。
3.1.2.4 樂觀鎖/悲觀鎖
悲觀鎖和樂觀鎖并不是某個具體的“鎖”而是一種是并發(fā)編程的基本概念。
悲觀鎖認(rèn)為對于同一個數(shù)據(jù)的并發(fā)操作,一定是會發(fā)生修改的,哪怕沒有修改,也會認(rèn)為修改。因此對于同一個數(shù)據(jù)的并發(fā)操作,悲觀鎖采取加鎖的形式。悲觀的認(rèn)為,不加鎖的并發(fā)操作一定會出問題。
樂觀鎖則與 Java 并發(fā)包中的 AtomicFieldUpdater 類似,也是利用 CAS 機(jī)制,并不會對數(shù)據(jù)加鎖,而是通過對比數(shù)據(jù)的時間戳或者版本號,來實現(xiàn)樂觀鎖需要的版本判斷。
3.1.2.5 公平鎖/非公平鎖
公平鎖是指多個線程按照申請鎖的順序來獲取鎖。
非公平鎖是指多個線程獲取鎖的順序并不是按照申請鎖的順序,有可能后申請的線程比先申請的線程優(yōu)先獲取鎖。
如果使用 synchronized 使用的是非公平鎖,是不可設(shè)置的,這也是主流操作系統(tǒng)線程調(diào)度的選擇。通用場景中,公平性未必有想象中的那么重要,Java 默認(rèn)的調(diào)度策略很少會導(dǎo)致 “饑餓”發(fā)生。非公平鎖的吞吐量大于公平鎖。
非公平鎖吞吐量大于公平鎖的原因:
比如A占用鎖的時候,B請求獲取鎖,發(fā)現(xiàn)被A占用之后,堵塞等待被喚醒,這個時候C同時來獲取A占用的鎖,如果是公平鎖C后來者發(fā)現(xiàn)不可用之后一定排在B之后等待被喚醒,而非公平鎖則可以讓C先用,在B被喚醒之前C已經(jīng)使用完成,從而節(jié)省了C等待和喚醒之間的性能消耗,這就是非公平鎖比公平鎖吞吐量大的原因。
3.2 ReentrantLock
ReentrantLock只能修飾代碼塊,使用ReentrantLock必須手動unlock釋放鎖,不然鎖永遠(yuǎn)會被占用。
3.2.1 ReentrantLock 使用
ReentrantLock reentrantLock = new ReentrantLock(true); // 設(shè)置為true為公平鎖,默認(rèn)是非公平鎖 reentrantLock.lock(); try {}finally {reentrantLock.unlock(); }3.2.2 ReentrantLock 優(yōu)勢
-
具備嘗試非阻塞地獲取鎖的特性:當(dāng)前線程嘗試獲取鎖,如果這一時刻鎖沒有被其他線程獲取到,則成功獲取并持有鎖;
-
能被中斷地獲取鎖的特性:與synchronized不同,獲取到鎖的線程能夠響應(yīng)中斷,當(dāng)獲取到鎖的線程被中斷時,中斷異常將會被拋出,同時鎖會被釋放;
-
超時獲取鎖的特性:在指定的時間范圍內(nèi)獲取鎖;如果截止時間到了仍然無法獲取鎖則返回。
3.2.3 ReentrantLock 注意事項
- 在finally中釋放鎖,目的是保證在獲取鎖之后,最終能夠被釋放;
- 不要將獲取鎖的過程寫在try塊內(nèi),因為如果在獲取鎖時發(fā)生了異常,異常拋出的同時,也會導(dǎo)致鎖無故被釋放;
- ReentrantLock提供了一個newCondition的方法,以便用戶在同一鎖的情況下可以根據(jù)不同的情況執(zhí)行等待或喚醒的動作;
3.3 synchronized和ReentrantLock區(qū)別
從性能角度,synchronized 早期的實現(xiàn)比較低效,對比 ReentrantLock,大多數(shù)場景性能都相差較大。但是在 Java 6 中對其進(jìn)行了非常多的改進(jìn),在高競爭情況下,ReentrantLock 仍然有一定優(yōu)勢。在大多數(shù)情況下,無需太糾結(jié)于性能,還是考慮代碼書寫結(jié)構(gòu)的便利性、可維護(hù)性等。
主要區(qū)別如下:
參考資料
《碼出高效:Java開發(fā)手冊》
Java核心技術(shù)36講:http://t.cn/EwUJvWA
Java中的鎖分類:https://www.cnblogs.com/qifengshi/p/6831055.html
課程推薦:
轉(zhuǎn)載于:https://my.oschina.net/u/3471412/blog/2906818
總結(jié)
以上是生活随笔為你收集整理的Java核心(三)并发中的线程同步与锁的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Disjoint Set
- 下一篇: JSON语法介绍