作者:湯圓
個人部落格:javalover.cc
前言
在前面併發的開篇,我們介紹過內建鎖synchronized
;
這節我們再介紹下顯式鎖Lock
顯式鎖包括:可重入鎖ReentrantLock
、讀寫鎖ReadWriteLock
關係如下所示:
簡介
顯式鎖和內建鎖最大的區別就是:顯式鎖需手動獲取鎖和釋放鎖,而內建鎖不需要
關於顯式鎖,本節會分別介紹可它的實現類 - 可重入鎖,以及它的相關類 - 讀寫鎖
-
可重入鎖,實現了顯式鎖,意思就是可重入的顯式鎖(內建鎖也是可重入的)
-
讀寫鎖,將顯式鎖分為讀寫分離,即讀讀可並行,多個執行緒同時讀不會阻塞(讀寫,寫寫還是序列)
下面讓我們開始吧
文章如果有問題,歡迎大家批評指正,在此謝過啦
目錄
- 可重入鎖 ReentrantLock
- 讀寫鎖 ReadWriteLock
- 區別
正文
1.可重入鎖 ReentrantLock
我們先來看下它的幾個方法:
-
public ReentrantLock()
;建構函式,預設構造非公平的鎖(可插隊,如果某個執行緒獲取鎖時,剛好鎖被釋放,那麼這個執行緒就會立馬獲得鎖,而不管佇列裡的執行緒是否在等待) -
public void lock()
:獲取鎖,以阻塞的方式(如果其他執行緒持有鎖,則阻塞當前執行緒,直到鎖被釋放); -
public void lockInterruptibly() throws InterruptedException
:獲取鎖,以可被中斷的方式(如果當前執行緒被中斷,則丟擲中斷異常); -
public boolean tryLock()
: 嘗試獲取鎖,如果鎖被其他執行緒持有,則立馬返回false -
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
:嘗試獲取鎖,並設定一個超時時間(如果超過這個時間,還沒獲取到鎖,則返回false) -
public void unlock()
: 釋放鎖
首先我們先看下它的構造方法,內部實現如下:
public ReentrantLock() {
sync = new NonfairSync();
}
可以看到,這裡建立了一個非公平鎖
公平鎖:如果獲取鎖時,被其他執行緒持有,則將當前執行緒放入等待佇列
非公平鎖:如果獲取鎖時,剛好鎖被釋放,那麼這個執行緒就會立馬獲得鎖,而不管佇列裡的執行緒是否在等待
非公平鎖的好處就是,可以減少執行緒的掛起和喚醒開銷
如果某個執行緒的執行任務所需時間很短,甚至比喚醒佇列中的執行緒所消耗的時間還短,那麼非公平鎖的優勢就很明顯
我們可以假設這樣一個情景:
- 執行緒A的任務執行耗時為10ms
- 而喚醒佇列中的執行緒B到執行真正去執行執行緒B的任務耗時為20ms
- 那麼當執行緒A去獲取鎖時,剛好鎖又被釋放,此時執行緒A搶先獲得鎖,並執行任務,然後釋放鎖
- 當執行緒A釋放鎖之後,佇列中當執行緒B才被喚醒正要去獲取鎖,那麼執行緒B被喚醒的這段時間CPU就沒有被浪費,從而提高了程式的效能
這也是為啥預設是非公平鎖的原因(一般情況下,非公平鎖的效能高於公平鎖)
那什麼時候應該用公平鎖呢?
- 持有鎖的時間較長,即執行緒的任務執行耗時較長
- 請求鎖的時間間隔較長
因為這種情況下,如果執行緒插隊獲取到鎖,結果任務還半天執行不完,那麼佇列中被喚醒的執行緒醒來發現鎖還是被佔有的,就會被再次放到佇列中(此時並不會提高效能,還有可能降低)
接下來我們看下關鍵的部分:獲取鎖
獲取鎖有多個方法,我們用程式碼來看下他們之間的區別
- 先來看下lock()方法,示例程式碼如下:
public class ReentrantLockDemo {
private Lock lock = new ReentrantLock();
private int i = 0;
public void add(){
lock.lock();
try {
i++;
}finally {
System.out.println(i);
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockDemo demo = new ReentrantLockDemo();
ExecutorService service = Executors.newFixedThreadPool(5);
for (int i = 0; i < 100; i++) {
service.submit(()->{
demo.add();
});
}
}
}
依次輸出1~100,這是因為lock()獲取鎖時,會以阻塞的方式來獲取
- 接下來看下 tryLock()方法,程式碼如下:
public class ReentrantLockDemo {
private Lock lock = new ReentrantLock();
private int i = 0;
public void tryAdd(){
if(lock.tryLock()){
try {
i++;
}finally {
System.out.println(i);
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockDemo demo = new ReentrantLockDemo();
ExecutorService service = Executors.newFixedThreadPool(5);
for (int i = 0; i < 100; i++) {
service.submit(()->{
demo.tryAdd();
});
}
}
}
執行發現,輸出永遠都少於100,是因為tryLock()如果獲取鎖失敗,會立馬返回false,而不是阻塞等待
- 最後我們來看下lockInterruptibly()方法,它也是阻塞獲取鎖,只是比lock()多了箇中斷異常,即獲取鎖時,如果執行緒被中斷,則丟擲中斷異常
public class ReentrantLockDemo {
private Lock lock = new ReentrantLock();
private int i = 0;
public void interruptAdd(){
try {
lock.lockInterruptibly();
i++;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(i);
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockDemo demo = new ReentrantLockDemo();
ExecutorService service = Executors.newFixedThreadPool(5);
for (int i = 0; i < 100; i++) {
// 第10次,立馬關閉執行緒池,停止所有的執行緒(包括正在執行的和正在等待的)
if (10 == i){
service.shutdownNow();
}
service.submit(()->{
demo.interruptAdd();
});
}
}
}
多執行幾次,有可能輸出如下:
1
2
3
4
5
6
6
6
6
6
java.lang.InterruptedException
at
......
這就是因為前面幾個都是正常獲取到鎖並執行了i++,但是後面的幾個執行緒因為被突然停止,所以丟擲中斷異常
- 最後就是釋放鎖, unlock()
這個就很簡單了,上面的程式碼都有涉及到這個釋放鎖
不過細心的朋友可能發現了,上面的unlock()都是在finally塊中編寫的
這是因為在獲取鎖並執行任務時,有可能丟擲異常,此時如果不把unlock()放到finally塊中,那麼鎖不被釋放,這在後期是一個很大的隱患(其他執行緒無法再次獲取到這個鎖,如果是lock()形式的獲取鎖,則執行緒會一直阻塞)
這也是顯式鎖無法完全替代內建鎖的一個原因,有危險
2. 讀寫鎖 ReadWriteLock
讀寫鎖內部就兩個方法,分別返回讀鎖和寫鎖
讀鎖屬於共享鎖,而寫鎖屬於獨佔鎖(前面介紹的可重入鎖和內建鎖也是獨佔鎖)
讀鎖允許多個執行緒同時獲取一個鎖,因為讀不會修改資料,它很適合讀多寫少的場合
下面我們用程式碼來看下
先看下讀鎖,程式碼如下:
public class ReadWriteLockDemo {
private int i = 0;
private Lock readLock;
private Lock writeLock;
public ReadWriteLockDemo() {
ReadWriteLock lock = new ReentrantReadWriteLock();
this.readLock = lock.readLock();
this.writeLock = lock.writeLock();
}
public void readFun(){
readLock.lock();
System.out.println("=== 獲取到 讀鎖 ===");
try {
System.out.println(i);
}finally {
readLock.unlock();
System.out.println("=== 釋放了 讀鎖 ===");
}
}
public static void main(String[] args) throws InterruptedException {
ReadWriteLockDemo demo = new ReadWriteLockDemo();
ExecutorService executors = Executors.newFixedThreadPool(2);
for (int i = 0; i < 10; i++) {
executors.submit(()->{
demo.readFun();
});
}
}
}
多次執行,有可能輸出下面的結果:
=== 獲取到 讀鎖 ===
0
=== 獲取到 讀鎖 ===
可以看到,兩個執行緒都獲取到了讀鎖,這就是讀鎖的優勢,多個執行緒同時讀
下面看下寫鎖,程式碼如下:(這裡用到了ReentrantReadWriteLock類,表示可重入的讀寫鎖)
public class ReadWriteLockDemo {
private int i = 0;
private Lock readLock;
private Lock writeLock;
public ReadWriteLockDemo() {
ReadWriteLock lock = new ReentrantReadWriteLock();
this.readLock = lock.readLock();
this.writeLock = lock.writeLock();
}
public void writeFun(){
writeLock.lock();
System.out.println("=== 獲取到 寫鎖 ===");
try {
i++;
System.out.println(i);
}finally {
writeLock.unlock();
System.out.println("=== 釋放了 寫鎖 ===");
}
}
public static void main(String[] args) throws InterruptedException {
ReadWriteLockDemo demo = new ReadWriteLockDemo();
ExecutorService executors = Executors.newFixedThreadPool(2);
for (int i = 0; i < 10; i++) {
executors.submit(()->{
demo.writeFun();
});
}
}
}
輸出如下:可以看到,寫鎖類似上面的重入鎖的lock()方法,阻塞獲取寫鎖
=== 獲取到 寫鎖 ===1=== 釋放了 寫鎖 ====== 獲取到 寫鎖 ===2=== 釋放了 寫鎖 ====== 獲取到 寫鎖 ===3=== 釋放了 寫鎖 ====== 獲取到 寫鎖 ===4=== 釋放了 寫鎖 ====== 獲取到 寫鎖 ===5=== 釋放了 寫鎖 ====== 獲取到 寫鎖 ===6=== 釋放了 寫鎖 ====== 獲取到 寫鎖 ===7=== 釋放了 寫鎖 ====== 獲取到 寫鎖 ===8=== 釋放了 寫鎖 ====== 獲取到 寫鎖 ===9=== 釋放了 寫鎖 ====== 獲取到 寫鎖 ===10=== 釋放了 寫鎖 ===
關於讀寫鎖,需要注意的一點是,讀鎖和寫鎖必須基於同一個ReadWriteLock類才有意義
如果讀鎖和寫鎖分別是從兩個ReadWrite Lock類中獲取的,那麼讀鎖和寫鎖就是完全無關的兩個鎖,也就不會起到鎖的作用(阻止其他執行緒訪問)
這就類似synchronized(a)和synchronized(b),分別鎖了兩個物件,此時單個執行緒是可以同時訪問這兩個鎖的
3. 區別
我們用表格來展示吧,細節如下:
鎖的特點 | 內建鎖 | 可重入鎖 | 讀寫鎖 |
---|---|---|---|
靈活性 | 低 | 高 | 高 |
公平性 | 不確定 | 非公平(預設)+公平 | 非公平(預設)+公平 |
定時性 | 無 | 可定時 | 可定時 |
中斷性 | 無 | 可中斷 | 可中斷 |
互斥性 | 互斥 | 互斥 | 讀讀共享,其他都互斥 |
建議優先選擇內建鎖,只有在內建鎖滿足不了需求時,再採用顯式鎖(比如可定時、可中斷、公平性)
如果是讀多寫少的場景(比如配置資料),推薦用讀寫鎖
總結
- 可重入鎖 ReentrantLock:需顯式獲取鎖和釋放鎖,切記要在finally塊中釋放鎖
- 讀寫鎖 ReadWriteLock:基於顯式鎖(顯式鎖有的它都有),多了讀寫分離,實現了讀讀共享(多個執行緒同時讀),其他都不共享(讀寫,寫寫)
- 區別:內建鎖不支援手動獲取/釋放鎖、公平性選擇、定時、中斷,顯式鎖支援
建議使用鎖時,優先考慮內建鎖
因為現在內建鎖的效能跟顯式鎖差別不大
而且顯式鎖因為需要手動釋放鎖(需在finally塊中釋放),所以會有忘記釋放的風險
如果是讀多寫少的場合,則推薦用讀寫鎖(成對的讀鎖和寫鎖需從同一個讀寫鎖類獲取)
參考內容:
- 《Java併發程式設計實戰》
- 《實戰Java高併發》
後記
最後,祝願所有人都心想事成,闔家歡樂