併發程式設計中,鎖是經常需要用到的,今天我們一起來看下Java中的鎖機制:synchronized和lock。
1. 鎖的種類
鎖的種類挺多,包括:自旋鎖、自旋鎖的其他種類、阻塞鎖、可重入鎖、讀寫鎖、互斥鎖、悲觀鎖、樂觀鎖、公平鎖、可重入鎖等等,其餘就不列出了。我們這邊重點看如下幾種:可重入鎖、讀寫鎖、可中斷鎖、公平鎖。
1.1 可重入鎖
如果鎖具備可重入性,則稱作為可重入鎖。synchronized和ReentrantLock都是可重入鎖,可重入性在我看來實際上表明瞭鎖的分配機制:基於執行緒的分配,而不是基於方法呼叫的分配。舉比如說,當一個執行緒執行到method1 的synchronized方法時,而在method1中會呼叫另外一個synchronized方法method2,此時該執行緒不必重新去申請鎖,而是可以直接執行方法method2。
1.2 讀寫鎖
讀寫鎖將對一個資源的訪問分成了2個鎖,如檔案,一個讀鎖和一個寫鎖。正因為有了讀寫鎖,才使得多個執行緒之間的讀操作不會發生衝突。ReadWriteLock
就是讀寫鎖,它是一個介面,ReentrantReadWriteLock實現了這個介面。可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖。
1.3 可中斷鎖
可中斷鎖,即可以中斷的鎖。在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。 如果某一執行緒A正在執行鎖中的程式碼,另一執行緒B正在等待獲取該鎖,可能由於等待時間過長,執行緒B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的執行緒中中斷它,這種就是可中斷鎖。
Lock介面中的lockInterruptibly()方法就體現了Lock的可中斷性。
1.4 公平鎖
公平鎖即儘量以請求鎖的順序來獲取鎖。同時有多個執行緒在等待一個鎖,當這個鎖被釋放時,等待時間最久的執行緒(最先請求的執行緒)會獲得該鎖,這種就是公平鎖。
非公平鎖即無法保證鎖的獲取是按照請求鎖的順序進行的,這樣就可能導致某個或者一些執行緒永遠獲取不到鎖。
synchronized
是非公平鎖,它無法保證等待的執行緒獲取鎖的順序。對於ReentrantLock
和ReentrantReadWriteLock
,預設情況下是非公平鎖,但是可以設定為公平鎖。
2. synchronized和lock的用法
2.1 synchronized
synchronized是Java的關鍵字,當它用來修飾一個方法或者一個程式碼塊的時候,能夠保證在同一時刻最多隻有一個執行緒執行該段程式碼。簡單總結如下四種用法。
2.1.1 程式碼塊
對某一程式碼塊使用,synchronized後跟括號,括號裡是變數,一次只有一個執行緒進入該程式碼塊。
public int synMethod(int m){
synchronized(m) {
//...
}
}
複製程式碼
2.1.2 方法宣告時
方法宣告時使用,放在範圍操作符之後,返回型別宣告之前。即一次只能有一個執行緒進入該方法,其他執行緒要想在此時呼叫該方法,只能排隊等候。
public synchronized void synMethod() {
//...
}
複製程式碼
2.1.3 synchronized後面括號裡是物件
synchronized後面括號裡是一物件,此時執行緒獲得的是物件鎖。
public void test() {
synchronized (this) {
//...
}
}
複製程式碼
2.1.4 synchronized後面括號裡是類
synchronized後面括號裡是類,如果執行緒進入,則執行緒在該類中所有操作不能進行,包括靜態變數和靜態方法,對於含有靜態方法和靜態變數的程式碼塊的同步,通常使用這種方式。
2.2 Lock
Lock介面主要相關的類和介面如下。
ReadWriteLock是讀寫鎖介面,其實現類為ReetrantReadWriteLock。ReetrantLock實現了Lock介面。
2.2.1 Lock
Lock中有如下方法:
public interface Lock {
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
複製程式碼
-
lock:用來獲取鎖,如果鎖被其他執行緒獲取,處於等待狀態。如果採用Lock,必須主動去釋放鎖,並且在發生異常時,不會自動釋放鎖。因此一般來說,使用Lock必須在try{}catch{}塊中進行,並且將釋放鎖的操作放在finally塊中進行,以保證鎖一定被被釋放,防止死鎖的發生。
-
lockInterruptibly:通過這個方法去獲取鎖時,如果執行緒正在等待獲取鎖,則這個執行緒能夠響應中斷,即中斷執行緒的等待狀態。
-
tryLock:tryLock方法是有返回值的,它表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他執行緒獲取),則返回false,也就說這個方法無論如何都會立即返回。在拿不到鎖時不會一直在那等待。
-
tryLock(long,TimeUnit):與tryLock類似,只不過是有等待時間,在等待時間內獲取到鎖返回true,超時返回false。
-
unlock:釋放鎖,一定要在finally塊中釋放
2.2.2 ReetrantLock
實現了Lock介面,可重入鎖,內部定義了公平鎖與非公平鎖。預設為非公平鎖:
public ReentrantLock() {
sync = new NonfairSync();
}
複製程式碼
可以手動設定為公平鎖:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
複製程式碼
2.2.3 ReadWriteLock
public interface ReadWriteLock {
Lock readLock(); //獲取讀鎖
Lock writeLock(); //獲取寫鎖
}
複製程式碼
一個用來獲取讀鎖,一個用來獲取寫鎖。也就是說將檔案的讀寫操作分開,分成2個鎖來分配給執行緒,從而使得多個執行緒可以同時進行讀操作。ReentrantReadWirteLock實現了ReadWirteLock介面,並未實現Lock介面。 不過要注意的是:
如果有一個執行緒已經佔用了讀鎖,則此時其他執行緒如果要申請寫鎖,則申請寫鎖的執行緒會一直等待釋放讀鎖。
如果有一個執行緒已經佔用了寫鎖,則此時其他執行緒如果申請寫鎖或者讀鎖,則申請的執行緒會一直等待釋放寫鎖。
2.2.4 ReetrantReadWriteLock
ReetrantReadWriteLock同樣支援公平性選擇,支援重進入,鎖降級。
public class RWLock {
static Map<String, Object> map = new HashMap<String, Object>();
static ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
static Lock r = rwLock.readLock();
static Lock w = rwLock.writeLock();
//讀
public static final Object get(String key){
r.lock();
try {
return map.get(key);
} finally {
r.unlock();
}
}
//寫
public static final Object put(String key, Object value){
w.lock();
try {
return map.put(key, value);
} finally {
w.unlock();
}
}
}
複製程式碼
只需在讀操作時獲取讀鎖,寫操作時獲取寫鎖。當寫鎖被獲取時,後續的讀寫操作都會被阻塞,寫鎖釋放後,所有操作繼續執行。
3. 兩種鎖的比較
3.1 synchronized和lock的區別
- Lock是一個介面,而synchronized是Java中的關鍵字,synchronized是內建的語言實現;
- synchronized在發生異常時,會自動釋放執行緒佔有的鎖,因此不會導致死鎖現象發生;而Lock在發生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖;
- Lock可以讓等待鎖的執行緒響應中斷,而synchronized卻不行,使用synchronized時,等待的執行緒會一直等待下去,不能夠響應中斷;
- 通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。
- Lock可以提高多個執行緒進行讀操作的效率。(可以通過readwritelock實現讀寫分離)
- 效能上來說,在資源競爭不激烈的情形下,Lock效能稍微比synchronized差點(編譯程式通常會盡可能的進行優化synchronized)。但是當同步非常激烈的時候,synchronized的效能一下子能下降好幾十倍。而ReentrantLock確還能維持常態。
3.2 效能比較
下面對synchronized與Lock進行效能測試,分別開啟10個執行緒,每個執行緒計數到1000000,統計兩種鎖同步所花費的時間。網上也能找到這樣的例子。
public class TestAtomicIntegerLock {
private static int synValue;
public static void main(String[] args) {
int threadNum = 10;
int maxValue = 1000000;
testSync(threadNum, maxValue);
testLocck(threadNum, maxValue);
}
//test synchronized
public static void testSync(int threadNum, int maxValue) {
Thread[] t = new Thread[threadNum];
Long begin = System.nanoTime();
for (int i = 0; i < threadNum; i++) {
Lock locks = new ReentrantLock();
synValue = 0;
t[i] = new Thread(() -> {
for (int j = 0; j < maxValue; j++) {
locks.lock();
try {
synValue++;
} finally {
locks.unlock();
}
}
});
}
for (int i = 0; i < threadNum; i++) {
t[i].start();
}
//main執行緒等待前面開啟的所有執行緒結束
for (int i = 0; i < threadNum; i++) {
try {
t[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("使用lock所花費的時間為:" + (System.nanoTime() - begin));
}
// test Lock
public static void testLocck(int threadNum, int maxValue) {
int[] lock = new int[0];
Long begin = System.nanoTime();
Thread[] t = new Thread[threadNum];
for (int i = 0; i < threadNum; i++) {
synValue = 0;
t[i] = new Thread(() -> {
for (int j = 0; j < maxValue; j++) {
synchronized(lock) {
++synValue;
}
}
});
}
for (int i = 0; i < threadNum; i++) {
t[i].start();
}
//main執行緒等待前面開啟的所有執行緒結束
for (int i = 0; i < threadNum; i++) {
try {
t[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("使用synchronized所花費的時間為:" + (System.nanoTime() - begin));
}
}
複製程式碼
測試結果的差異還是比較明顯的,Lock的效能明顯高於synchronized。本次測試基於JDK1.8。
使用lock所花費的時間為:436667997
使用synchronized所花費的時間為:616882878
複製程式碼
JDK1.5中,synchronized是效能低效的。因為這是一個重量級操作,它對效能最大的影響是阻塞的是實現,掛起執行緒和恢復執行緒的操作都需要轉入核心態中完成,這些操作給系統的併發性帶來了很大的壓力。相比之下使用Java提供的Lock物件,效能更高一些。多執行緒環境下,synchronized的吞吐量下降的非常嚴重,而ReentrankLock則能基本保持在同一個比較穩定的水平上。
到了JDK1.6,發生了變化,對synchronize加入了很多優化措施,有自適應自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等。導致在JDK1.6上synchronize的效能並不比Lock差。官方也表示,他們也更支援synchronize,在未來的版本中還有優化餘地,所以還是提倡在synchronized能實現需求的情況下,優先考慮使用synchronized來進行同步。
4. 總結
本文主要對併發程式設計中的鎖機制synchronized和lock,進行詳解。synchronized是基於JVM實現的,內建鎖,Java中的每一個物件都可以作為鎖。對於同步方法,鎖是當前例項物件。對於靜態同步方法,鎖是當前物件的Class物件。對於同步方法塊,鎖是Synchonized括號裡配置的物件。Lock是基於在語言層面實現的鎖,Lock鎖可以被中斷,支援定時鎖。Lock可以提高多個執行緒進行讀操作的效率。通過對比得知,Lock的效率是明顯高於synchronized關鍵字的,一般對於資料結構設計或者框架的設計都傾向於使用Lock而非Synchronized。