java多線程Lock接口簡介使用與synchronized對比 多線程下篇(三)


前面的介紹中,對於顯式鎖的概念進行了簡單介紹
顯式鎖的概念,是基於JDK層面的實現,是接口,通過這個接口可以實現同步訪問
而不同於synchronized關鍵字,他是Java的內置特性,是基於JVM的實現
image_5c7f20d6_7841
Lock接口的核心概念很簡單,只有如下幾個方法
image_5c7f20d6_1834
按照邏輯可以進行如下划分
image_5c7f20d6_415

lock()

Lock接口,所以synchronized關鍵字更為靈活的一種同步方案,在實際使用中,自然是能夠替代synchronized關鍵字的
(ps:盡管你不需要總是使用顯式鎖,顯式鎖與隱式鎖各有利弊,但是在語法上是的確可以替代的)
synchronized關鍵字是阻塞式的獲取鎖
lock方法就是這一邏輯的體現,也就是說對於lock()方法,如果獲取不到鎖,那么將會進入阻塞狀態,與synchronized關鍵字一樣

lockInterruptibly()

Lock()方法是一種阻塞式的,另外Lock接口還提供了可中斷的lock獲取方法,先看下測試例子
package test2;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class T28 {
private static final Lock LOCK = new ReentrantLock();
public static void main(String[] args) {
//線程A獲取加鎖之后,持有五秒鍾
Thread threadA = new Thread(() -> {
LOCK.lock();
try {
System.out.println(Thread.currentThread().getName() + " " + System.currentTimeMillis());
System.out.println(Thread.currentThread().getName() + " sleep");
TimeUnit.SECONDS.sleep(10);
System.out.println(Thread.currentThread().getName() + " " + System.currentTimeMillis());
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " interrupt");
} finally {
LOCK.unlock();
}
}, "thread-A");
threadA.start();
//線程B開始后,嘗試獲取鎖
Thread threadB = new Thread(() -> {
LOCK.lock();
try {
System.out.println(Thread.currentThread().getName() + " " + System.currentTimeMillis());
System.out.println(Thread.currentThread().getName() + " working");
} finally {
LOCK.unlock();
}
}, "thread-B");
threadB.start();
//為了確保上面的任務都開始了,主線程sleep 1s
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
threadB.interrupt();
}
}
示例邏輯
兩個線程A和B,使用同一把鎖
A線程獲取鎖后,休眠10s,緊接着B嘗試獲取鎖
為了保證前面的任務都開始了,主線程sleep 1s后,將線程B進行中斷
對於lock方法,如同synchronized關鍵字,是阻塞式的,通過執行來看,可以發現,在A持有鎖期間,線程B也是一直阻塞的,是不能夠獲取到鎖,也不能被中斷(上面示例中調用interrupt()沒有任何的反應)
image_5c7f20d6_4536
將代碼稍作修改,也就是將lock方法修改為lockInterruptibly()方法,其他暫時不變
image_5c7f20d6_7bb5
再次運行,你會發現馬上就被中斷了,而不是傻傻的等待A結束
當然,因為根本都沒有獲取到鎖,所以在finally中嘗試unlock時,將會拋出異常,這個暫時不管了,通過這個例子可以看得出來
對於lockInterruptibly方法,這是一個“可中斷的鎖獲取操作
image_5c7f20d6_1200
小結
lockInterruptibly就是一個可中斷的鎖獲取操作,在嘗試獲取鎖的過程中,如果不能夠獲取到,如果被中斷,那么它將能夠感知到這個中斷,而不是一直阻塞下去
如果鎖不可用(被其他線程持有),除非發生以下事件,否則將會等待
  • 該線程成功獲得鎖
  • 發生中斷
如果當前線程遇到下面的事件,則將拋出 InterruptedException,並清除當前線程的已中斷狀態。
  • 在進入此方法時已經設置了該線程的中斷狀態
  • 在獲取鎖時被中斷
從上面的分析可以看得出來,如果什么都沒發生,這個方法與lock方法並沒有什么區別,就是在等待獲取鎖,獲取不到將會阻塞
他只是額外的對可中斷提供了支持  

unlock()

unlock並沒有什么特殊的,他替代了synchronized關鍵字隱式的解鎖操作
通常需要在finally中確保unlock操作會被執行,之前提到過,對於synchronized關鍵字解鎖是隱式的,也是必然的,即使出現錯誤,JVM也會保障能夠正確的解鎖
但是對於Lock接口提供的unlock操作,則必須自己確保能夠正確的解鎖  

tryLock()

相對於synchronized,Lock接口另一大改進就是try lock
顧名思義,嘗試獲取鎖,既然是嘗試,那顯然並不會勢在必得
tryLock方法就是一次嘗試,如果鎖可用,則獲取鎖,並立即返回值 true。如果鎖不可用,則此方法將立即返回值 false
也就是說方法會立即返回,如果獲取到鎖返回true,否則返回false,不管如何都是立馬返回
典型的用法就是如下所示,下面的代碼還能夠確保如果沒有獲取鎖,不會試圖進行unlock操作
Lock lock = ...;
if (lock.tryLock()) {
try {
// manipulate protected state
} finally {
lock.unlock();
}
} else {
// perform alternative actions
}
tryLock只是一次嘗試,如果你需要不斷地進行嘗試,那么可以使用while替代if的條件判斷
盡管tryLock只是一次的測試,但是可以借助於循環(有限或者無限)進行多次測試  

tryLock(long time, TimeUnit unit)

對於TryLock還有可中斷、配置超時時間的版本
boolean tryLock(long time,
                TimeUnit unit)
                throws InterruptedException
兩個參數,第一個為值,第二個為第一個參數的單位,比如1,單位秒,或者2 ,單位分鍾
在指定的超時時間內,如果能夠獲取到鎖,那么將會返回true;
如果超過了指定的時間,但是卻不能獲取到鎖,那么將會返回false;
另外很顯然,這個方法是可中斷的,也就是說如果嘗試過程中,出現了中斷,那么他將會拋出InterruptedException
所以,對於這個方法,他會一直嘗試獲取鎖(也可以認為是一定時長內的“阻塞”,當然可以被中斷),除非:
  • 該線程成功獲得鎖
  • 超過了超時時長
  • 該線程被中斷
可以認為是lockInterruptibly的限時版本
如果沒有發生中斷,也認為他就是“定時版本的lock()”
不管怎么理解,只需要記住:他會在一定時長內嘗試進行鎖的獲取,也支持中斷

鎖小結

對於lock方法和unlock方法,就是類似於synchronized關鍵字的加鎖和解鎖,並沒有什么特別的
其他幾個方法是Lock接口針對於鎖獲取的阻塞以及可中斷兩個方面進行了拓展
隱式鎖的阻塞以及不可中斷,導致一旦開始嘗試獲取,那么則沒辦法喚醒,將會一直等待,除非獲得
  • lockInterruptibly()是阻塞式的,如果獲取不到會一直等待,但是他是可中斷的,能夠通過阻塞打破這種等待
  • tryLock()不會進行任何阻塞,只是嘗試獲取一下,能獲取到就獲取,獲取不到就false,拉倒
  • tryLock(long time, TimeUnit unit),即是可中斷的,又是限時阻塞的,即使不中斷,也不會一直阻塞,即使處於阻塞中(超時時長還沒到),也可以隨時中斷
對於lockInterruptibly()方法以及tryLock(long time, TimeUnit unit),都支持中斷,但是需要注意:
在某些實現中可能無法中斷鎖獲取,即使可能,該操作的開銷也很大  

Condition

在隱式鎖的邏輯中,借助於Java底層機制,每個對象都有一個相關聯的鎖與監視器
對於synchronized的隱式鎖邏輯就是借助於鎖與監視器,從而進行線程的同步與通信協作
在顯式鎖中,Lock接口提供了synchronized的語意,對於監視器的概念,則借助於Condition,但是很顯然,Condition也是與鎖關聯的
Lock接口提供了方法Condition newCondition();
Condition也是一個接口,他定義了相關的監視器方法
在顯式鎖中,可以定義多個Condition,也就是一個鎖,可以對應多個監視器,可以更加細粒度的進行同步協作的處理

總結

Lock接口提供了相對於synchronized關鍵字,而更為靈活的一種同步手段
它的核心與本質仍舊是為了線程的同步與協作通信
所以它的核心仍舊是鎖與監視器,也就是Lock接口與Condition接口
但是靈活是有代價的,所以並不需要在所有的地方都嘗試使用顯式鎖,如果場景滿足需要,synchronized仍舊是一種很好的解決方案(也是應該被優先考慮的一種方式)
與synchronized再次對比下
  • synchronized是JVM底層實現的,Lock是JDK接口層面的
  • synchronized是隱式的,Lock是顯式的,需要手動加鎖與解鎖
  • synchronized烏無論如何都會釋放,即使出現錯誤,Lock需要自己保障正確釋放
  • synchronized是阻塞式的獲取鎖,Lock可以阻塞獲取,可中斷,還可以嘗試獲取,還可以設置超時等待獲取
  • synchronized無法判斷鎖的狀態,Lock可以進行判斷
  • synchronized可重入,不可中斷,非公平,Lock可重入,可中斷、可配置公平性(公平和非公平都可以)
  • 如果競爭不激烈,兩者的性能是差不多的,可是synchronized的性能還在不斷的優化,當競爭資源非常激烈時(即有大量線程同時競爭),此時Lock的性能要遠遠優於synchronized   
  • 等   
對於Lock接口,他仍舊是一個對象,所以他是否可以用來作為鎖以及調用監視器方法(用在synchronized(lock)中)?
這邏輯上是沒問題的,但是最好不要那么做,因為很容易引起混淆的,不管是維護上還是易讀性上都有很大的問題
在lock上調用他的監視器方法,與借助於lock實現線程的同步,本質上是沒有什么關系的
image_5c7f20d6_6b7b
盡管看起來Lock是那么的優秀,但是還是要再次提醒,除非synchronized真的不行,否則你應該使用synchronized而不是Lock
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM