Java進階知識--Synchronized、Lock、ReentrantLock的區別
最近在看《Java併發程式設計的藝術》,書中不少知識是更深入的去講解我們平時經常使用的併發實現機制,介紹了它們的實現原理和區別,讀完之後真的有種醍醐灌頂的感覺,突然就好像明白了這些實現到底是幹什麼用、什麼時候去用,今天我就來總結一下其中的一個知識點——Synchronized、Lock、ReentrantLock的區別。
1. Synchronized
當它用來修飾一個方法或者一個程式碼塊的時候,能夠保證在同一時刻最多隻有一個執行緒執行該段程式碼,它是在 軟體層面依賴JVM實現同步。synchronized方法或語句的使用提供了對與每個物件相關的隱式監視器鎖的訪問,但卻強制所有鎖獲取和釋放均要出現在一個塊結構中:
當獲取了多個鎖時,它們必須以相反的順序釋放,且必須在與所有鎖被獲取時相同的詞法範圍內釋放所有鎖。
synchronized 方法的缺陷:
若將一個大的方法宣告為synchronized將會大大影響效率,典型地,若將執行緒類的方法run()宣告為synchronized,由於線上程的整個生命期內它一直在執行,因此將導致它對本類任何synchronized方法的呼叫都永遠不會成功。
解決synchronized 方法的缺陷:
通過 synchronized關鍵字來宣告synchronized 塊
synchronized(syncObject) {
// 訪問或修改被鎖保護的共享狀態
}
其中的程式碼必須獲得物件 syncObject(類例項或類)的鎖方能執行。由於可以針對任意程式碼塊,且可任意指定上鎖的物件,故靈活性較高。
當兩個併發執行緒訪問同一個物件中的這個synchronized(this)同步程式碼塊時,一個時間內只能有一個執行緒得到執行。另一個執行緒必須等待當前執行緒執行完這個程式碼塊以後才能執行該程式碼塊。
當一個執行緒訪問物件的一個synchronized(this)同步程式碼塊時,另一個執行緒仍然可以訪問該物件中的非synchronized(this)同步程式碼塊。其他執行緒對物件中所有其它synchronized(this)同步程式碼塊的訪問將被阻塞。
如果執行緒進入由執行緒已經擁有的監控器保護的 synchronized 塊,就允許執行緒繼續進行,當執行緒退出第二個(或者後續)synchronized塊的時候,不釋放鎖,只有執行緒退出它進入的監控器保護的第一個synchronized塊時,才釋放鎖。
在修飾程式碼塊的時候需要一個reference物件作為鎖的物件.
在修飾方法的時候預設是當前物件作為鎖的物件.
在修飾類時候預設是當前類的Class物件作為鎖的物件.
2. Lock
Lock介面實現提供了比使用synchronized方法和語句可獲得的更廣泛的鎖定操作。此實現允許更靈活的結構,可以具有差別很大的屬性,可以支援多個相關的Condition物件。在硬體層面依賴特殊的CPU指令實現同步更加靈活。
什麼是Condition
Condition介面將Object監視器方法(wait、notify 和 notifyAll)分解成截然不同的物件,以便通過將這些物件與任意 Lock實現組合使用,為每個物件提供多個等待set(wait-set)。其中,Lock替代了synchronized方法和語句的使用,Condition替代了 Object監視器方法的使用。
雖然synchronized方法和語句的範圍機制使得使用監視器鎖程式設計方便了很多,而且還幫助避免了很多涉及到鎖的常見程式設計錯誤,但有時也需要以更為靈活的方式使用鎖。例如,某些遍歷併發訪問的資料結果的演算法要求使用”hand-over-hand”或”chain locking”:獲取節點 A的鎖,然後再獲取節點B的鎖,然後釋放A並獲取C,然後釋放B並獲取D,依此類推。Lock介面的實現允許鎖在不同的作用範圍內獲取和釋放,並允許以任何順序獲取和釋放多個鎖,從而支援使用這種技術。
隨著靈活性的增加,也帶來了更多的責任。不使用塊結構鎖就失去了使用synchronized方法和語句時會出現的鎖自動釋放功能。在大多數情況下,應該使用以下語句:
Lock l = ...; //lock介面的實現類物件
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}
在java.util.concurrent.locks包中有很多Lock的實現類,常用的有ReentrantLock、ReadWriteLock(實現類ReentrantReadWriteLock).它們是具體實現類,不是Java語言關鍵字。
3. ReentrantLock
一個可重入的互斥鎖Lock,它具有與使用synchronized方法和語句所訪問的隱式監視器鎖相同的一些基本行為和語義,但功能更強大。
最典型的程式碼如下:
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}
ReentrantLock的lock機制有2種,忽略中斷鎖和響應中斷鎖,這給我們帶來了很大的靈活性。比如:如果A、B 2個執行緒去競爭鎖,A執行緒得到了鎖,B執行緒等待,但是A執行緒這個時候實在有太多事情要處理,就是一直不返回,B執行緒可能就會等不及了,想中斷自己,不再等待這個鎖了,轉而處理其他事情。這個時候ReentrantLock就提供了2種機制,第一,B執行緒中斷自己(或者別的執行緒中斷它),但是ReentrantLock不去響應,繼續讓B執行緒等待,你再怎麼中斷,我全當耳邊風(synchronized原語就是如此);第二,B執行緒中斷自己(或者別的執行緒中斷它),ReentrantLock 處理了這個中斷,並且不再等待這個鎖的到來,完全放棄。
ReentrantLock相對於synchronized多了三個高階功能:
1.等待可中斷
在持有鎖的執行緒長時間不釋放鎖的時候,等待的執行緒可以選擇放棄等待.
tryLock(long timeout, TimeUnit unit)
2.公平鎖
按照申請鎖的順序來一次獲得鎖稱為公平鎖.synchronized的是非公平鎖,ReentrantLock可以通過建構函式實現公平鎖.
new RenentrantLock(boolean fair)
公平鎖和非公平鎖。這2種機制的意思從字面上也能瞭解個大概:即對於多執行緒來說,公平鎖會依賴執行緒進來的順序,後進來的執行緒後獲得鎖。而非公平鎖的意思就是後進來的鎖也可以和前邊等待鎖的執行緒同時競爭鎖資源。對於效率來講,當然是非公平鎖效率更高,因為公平鎖還要判斷是不是執行緒佇列的第一個才會讓執行緒獲得鎖。
3.繫結多個Condition
通過多次newCondition可以獲得多個Condition物件,可以簡單的實現比較複雜的執行緒同步的功能.
synchronized和lock的用法與區別
1.synchronized是託管給JVM執行的,而Lock是Java寫的控制鎖的程式碼。
2.synchronized原始採用的是CPU悲觀鎖機制,即執行緒獲得的是獨佔鎖。獨佔鎖意味著其他執行緒只能依靠阻塞來等待執行緒釋放鎖。而在CPU轉換執行緒阻塞時會引起執行緒上下文切換,當有很多執行緒競爭鎖的時候,會引起CPU頻繁的上下文切換導致效率很低。
3.Lock用的是樂觀鎖方式。每次不加鎖而是假設沒有衝突而去完成某項操作,如果因為衝突失敗就重試,直到成功為止。
4.ReentrantLock必須在finally中釋放鎖,否則後果很嚴重,編碼角度來說使用synchronized更加簡單,不容易遺漏或者出錯。
5.ReentrantLock提供了可輪詢的鎖請求,他可以嘗試的去取得鎖,如果取得成功則繼續處理,取得不成功,可以等下次執行的時候處理,所以不容易產生死鎖,而synchronized則一旦進入鎖請求要麼成功,要麼一直阻塞,所以更容易產生死鎖。
6.synchronized的話,鎖的範圍是整個方法或synchronized塊部分;而Lock因為是方法呼叫,可以跨方法,靈活性更大
一般情況下都是用synchronized原語實現同步,除非下列情況使用ReentrantLock:
1.某個執行緒在等待一個鎖的控制權的這段時間需要中斷
2.需要分開處理一些wait-notify,ReentrantLock裡面的Condition應用,能夠控制notify哪個執行緒
3.具有公平鎖功能,每個到來的執行緒都將排隊等候