java鎖synchronized和lock的區別

> 典型回答
synchronized 是 Java 內建的同步機制,它提供了互斥的語義和可見性,當一個線程已經獲取當前鎖時,其他試圖獲取的線程只能等待或者阻塞在那裏。
synchronized可以用來修飾方法,也可以使用在特定的代碼塊兒上,本質上 synchronized 方法等同於把方法全部語句用 synchronized 塊包起來。

ReentrantLock,通常翻譯爲再入鎖,是 Java 5 提供的鎖實現,它的語義和 synchronized 基本相同。
再入鎖通過代碼直接調用 lock() 方法獲取,代碼書寫也更加靈活。與此同時,ReentrantLock 提供了很多實用的方法,能夠實現很多 synchronized 無法做到的細節控制,比如可以控制 fairness,也就是公平性,或者利用定義條件等。
但是,編碼中也需要注意,必須要明確調用 unlock() 方法釋放,不然就會一直持有該鎖。synchronized 和 ReentrantLock 的性能不能一概而論,早期版本 synchronized 在很多場景下性能相差較大,在後續版本進行了較多改進,在低競爭場景中表現可能優於 ReentrantLock。
> 理解什麼是線程安全。
線程安全是一個多線程環境下正確性的概念,也就是保證多線程環境下共享的、可修改的狀態的正確性,這裏的狀態反映在程序中其實可以看作是數據。
換個角度來看,如果狀態不是共享的,或者不是可修改的,也就不存在線程安全問題,進而可以推理出保證線程安全的兩個辦法:

	封裝:通過封裝,我們可以將對象內部狀態隱藏、保護起來。
	不可變:還記得我們在專欄第 3 講強調的 final 和 immutable 嗎,就是這個道理,Java 語言目前還沒有真正意義上的原生不可變,但是未來也許會引入。
>> 線程安全需要保證幾個基本特性:
原子性,簡單說就是相關操作不會中途被其他線程干擾,一般通過同步機制實現。
可見性,是一個線程修改了某個共享變量,其狀態能夠立即被其他線程知曉,通常被解釋爲將線程本地狀態反映到主內存上,volatile 就是負責保證可見性的。
有序性,是保證線程內串行語義,避免指令重排等。
>synchronized、ReentrantLock 等機制的基本使用與案例。
> 掌握 synchronized、ReentrantLock 底層實現
synchronized 代碼塊是由一對兒 monitorenter/monitorexit 指令實現的,Monitor 對象是同步的基本實現單元
在 Java 6 之前,Monitor 的實現完全是依靠操作系統內部的互斥鎖,因爲需要進行用戶態到內核態的切換,所以同步操作是一個無差別的重量級操作。
現代的(Oracle)JDK 中,JVM 對此進行了大刀闊斧地改進,提供了三種不同的 Monitor 實現,也就是常說的三種不同的鎖:偏斜鎖(Biased Locking)、輕量級鎖和重量級鎖,大大改進了其性能。
> 其他lock

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-llVXiJZZ-1586533088986)(http://47.104.234.149:8090/upload/2020/4/image-5992b8873995426f8ce34bee6c922141.png)]

StampedLock,在提供類似讀寫鎖的同時,還支持優化讀模式。
優化讀基於假設,大多數情況下讀操作並不會和寫操作衝突,其邏輯是先試着讀,然後通過 validate 方法確認是否進入了寫模式,
如果沒有進入,就成功避免了開銷;如果進入,則嘗試獲取讀鎖。

ReadWriteLock
雖然 ReentrantLock 和 synchronized 簡單實用,但是行爲上有一定侷限性,通俗點說就是“太霸道”,要麼不佔,要麼獨佔。
實際應用場景中,有的時候不需要大量競爭的寫操作,而是以併發讀取爲主,如何進一步優化併發操作的粒度呢?
Java 併發包提供的讀寫鎖等擴展了鎖的能力,它所基於的原理是多個讀操作是不需要互斥的,因爲讀操作並不會更改數據,所以不存在互相干擾。
而寫操作則會導致併發一致性的問題,所以寫線程之間、讀寫線程之間,需要精心設計的互斥邏輯。
在運行過程中,如果讀鎖試圖鎖定時,寫鎖是被某個線程持有,讀鎖將無法獲得,而只好等待對方操作結束,這樣就可以自動保證不會讀取到有爭議的數據。
讀寫鎖看起來比 synchronized 的粒度似乎細一些,但在實際應用中,其表現也並不盡如人意,主要還是因爲相對比較大的開銷。
> 理解鎖膨脹、降級;
所謂鎖的升級、降級,就是 JVM 優化 synchronized 運行的機制,當 JVM 檢測到不同的競爭狀況時,會自動切換到適合的鎖實現,這種切換就是鎖的升級、降級。
1: 當沒有競爭出現時,默認會使用偏斜鎖。JVM 會利用 CAS 操作(compare and swap),在對象頭上的 Mark Word 部分設置線程 ID,以表示這個對象偏向於當前線程,所以並不涉及真正的互斥鎖。這樣做的假設是基於在很多應用場景中,大部分對象生命週期中最多會被一個線程鎖定,使用偏斜鎖可以降低無競爭開銷。
2: 如果有另外的線程試圖鎖定某個已經被偏斜過的對象,JVM 就需要撤銷(revoke)偏斜鎖,並切換到輕量級鎖實現。輕量級鎖依賴 CAS 操作 Mark Word 來試圖獲取鎖,如果重試成功,就使用普通的輕量級鎖;否則,進一步升級爲重量級鎖。
3: 當 JVM 進入安全點(SafePoint)的時候,會檢查是否有閒置的 Monitor,然後試圖進行降級。
>> 理解偏斜鎖、自旋鎖、輕量級鎖、重量級鎖等概念。
>> 偏向鎖
大多數情況下鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得。偏向鎖的目的是在某個線程獲得鎖之後,消除這個線程鎖重入(CAS)的開銷,看起來讓這個線程得到了偏護。另外,JVM對那種會有多線程加鎖,但不存在鎖競爭的情況也做了優化,聽起來比較拗口,但在現實應用中確實是可能出現這種情況,因爲線程之前除了互斥之外也可能發生同步關係,被同步的兩個線程(一前一後)對共享對象鎖的競爭很可能是沒有衝突的。對這種情況,JVM用一個epoch表示一個偏向鎖的時間戳(真實地生成一個時間戳代價還是蠻大的,因此這裏應當理解爲一種類似時間戳的identifier)
>>> 偏向鎖的獲取
當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏存儲鎖偏向的線程ID,以後該線程在進入和退出同步塊時不需要花費CAS操作來加鎖和解鎖,而只需簡單的測試一下對象頭的Mark Word裏是否存儲着指向當前線程的偏向鎖,如果測試成功,表示線程已經獲得了鎖,如果測試失敗,則需要再測試下Mark Word中偏向鎖的標識是否設置成1(表示當前是偏向鎖),如果沒有設置,則使用CAS競爭鎖,如果設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。
>>> 偏向鎖的撤銷
偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節碼正在執行),它會首先暫停擁有偏向鎖的線程,然後檢查持有偏向鎖的線程是否活着,如果線程不處於活動狀態,則將對象頭設置成無鎖狀態,如果線程仍然活着,擁有偏向鎖的棧會被執行,遍歷偏向對象的鎖記錄,棧中的鎖記錄和對象頭的Mark Word,要麼重新偏向於其他線程,要麼恢復到無鎖或者標記對象不適合作爲偏向鎖,最後喚醒暫停的線程。
>>> 偏向鎖的設置
關閉偏向鎖:偏向鎖在Java 6和Java 7裏是默認啓用的,但是它在應用程序啓動幾秒鐘之後才激活,如有必要可以使用JVM參數來關閉延遲-XX:BiasedLockingStartupDelay = 0。如果你確定自己應用程序裏所有的鎖通常情況下處於競爭狀態,可以通過JVM參數關閉偏向鎖-XX:-UseBiasedLocking=false,那麼默認會進入輕量級鎖狀態。
>> 自旋鎖
線程的阻塞和喚醒需要CPU從用戶態轉爲核心態,頻繁的阻塞和喚醒對CPU來說是一件負擔很重的工作。同時我們可以發現,很多對象鎖的鎖定狀態只會持續很短的一段時間,例如整數的自加操作,在很短的時間內阻塞並喚醒線程顯然不值得,爲此引入了自旋鎖。

所謂“自旋”,就是讓線程去執行一個無意義的循環,循環結束後再去重新競爭鎖,如果競爭不到繼續循環,循環過程中線程會一直處於running狀態,但是基於JVM的線程調度,會出讓時間片,所以其他線程依舊有申請鎖和釋放鎖的機會。

自旋鎖省去了阻塞鎖的時間空間(隊列的維護等)開銷,但是長時間自旋就變成了“忙式等待”,忙式等待顯然還不如阻塞鎖。所以自旋的次數一般控制在一個範圍內,例如10,100等,在超出這個範圍後,自旋鎖會升級爲阻塞鎖。
> 輕量級鎖

加鎖

線程在執行同步塊之前,JVM會先在當前線程的棧楨中創建用於存儲鎖記錄的空間,並將對象頭中的Mark Word複製到鎖記錄中,官方稱爲Displaced Mark Word。然後線程嘗試使用CAS將對象頭中的Mark Word替換爲指向鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,則自旋獲取鎖,當自旋獲取鎖仍然失敗時,表示存在其他線程競爭鎖(兩條或兩條以上的線程競爭同一個鎖),則輕量級鎖會膨脹成重量級鎖。

解鎖

輕量級解鎖時,會使用原子的CAS操作來將Displaced Mark Word替換回到對象頭,如果成功,則表示同步過程已完成。如果失敗,表示有其他線程嘗試過獲取該鎖,則要在釋放鎖的同時喚醒被掛起的線程。

重量級鎖

重量鎖在JVM中又叫對象監視器(Monitor),它很像C中的Mutex,除了具備Mutex(0|1)互斥的功能,它還負責實現了Semaphore(信號量)的功能,也就是說它至少包含一個競爭鎖的隊列,和一個信號阻塞隊列(wait隊列),前者負責做互斥,後一個用於做線程同步。

掌握併發包中 java.util.concurrent.lock 各種不同實現和案例分析。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章