一、簡介
在之前的線程系列文章中,我們介紹到了使用synchronized
關鍵字可以實現線程同步安全的效果,以及採用wait()
、notify()
和notifyAll()
方法,可以實現多個線程之間的通信協調,基本可以滿足併發編程的需求。
但是採用synchronized
進行加鎖,這種鎖一般都比較重,裏面的實現機制也非常複雜,同時獲取鎖時必須一直等待,沒有額外的嘗試機制,如果編程不當,可能就容易發生死鎖現象。
從 JDK 1.5 開始,引入了一個高級的處理併發的java.util.concurrent
包,它提供了大量更高級的併發功能,能大大的簡化多線程程序的編寫。
比如我們今天要介紹的java.util.concurrent.locks
包提供的ReentrantLock
類,一個可重入的互斥鎖,它具有與使用synchronized
加鎖一樣的特性,並且功能更加強大。
下面我們一起來學習一下ReentrantLock
類的基本使用。
二、ReentrantLock 基本用法
在介紹ReentrantLock
之前,我們先來看一下傳統的使用synchronized
對方法進行加鎖的示例。
public class Counter {
private int count;
public void add() {
synchronized(this) {
count ++;
System.out.println("ThreadName:" + Thread.currentThread().getName() + ", count:" + getCount());
}
}
public int getCount() {
return count;
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// 創建5個線程,同時對count進行加一操作
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
counter.add();
}
}).start();
}
// 假設休眠1秒,5個線程執行完畢
Thread.sleep(1000);
System.out.println("count:" + counter.getCount());
}
輸出結果如下:
ThreadName:Thread-0, count:1
ThreadName:Thread-1, count:2
ThreadName:Thread-2, count:3
ThreadName:Thread-3, count:4
ThreadName:Thread-4, count:5
count:5
如果用ReentrantLock
替代,只需要將Counter
中的代碼改造爲如下:
public class Counter {
private final Lock lock = new ReentrantLock();
private int count;
public void add() {
// 加鎖
lock.lock();
try {
count ++;
System.out.println("ThreadName:" + Thread.currentThread().getName() + ", count:" + getCount());
} finally {
// 釋放鎖
lock.unlock();
}
}
public int getCount() {
return count;
}
}
運行程序,結果與上面一致,可以證明:ReentrantLock
具備與synchronized
一樣的加鎖功能。
同時,ReentrantLock
還具備在指定的時間內嘗試獲取鎖的機制,比如下面這行代碼:
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
...
} finally {
lock.unlock();
}
}
嘗試在 3 秒內獲取鎖,如果獲取不到就返回false
,程序不需要無限等待下去,這個功能在實際開發中使用非常的廣泛。
從上面的示例代碼,我們可以總結出synchronized
和ReentrantLock
有以下幾點不一樣。
ReentrantLock
需要手動調用加鎖方法;而synchronized
不需要,它採用了隱藏的加鎖方式,藉助 jvm 來實現synchronized
不需要考慮異常;而ReentrantLock
獲取鎖之後,要在finally
中正確的釋放鎖,否則會影響其它線程ReentrantLock
擁有嘗試獲取鎖的超時機制,利用它可以避免無限等待;而synchronized
不具備synchronized
是 Java 語言層面提供的語法;而ReentrantLock
是 Java 代碼實現的可重入鎖
因此,在併發編程中,使用ReentrantLock
比直接使用synchronized
更靈活、更安全,採用tryLock(long time, TimeUnit unit)
方法,即使未獲取到鎖也不會導致死鎖。
三、ReentrantLock 和 synchronized 持有的對象監視器是同一個嗎?
可能有的同學會發出這樣的一個問題,使用ReentrantLock
進行加鎖和使用synchronized
加鎖,兩者持有的對象監視器是同一個嗎?
下面我們一起來看一個例子。
public class Counter {
private final Lock lock = new ReentrantLock();
private int count;
public synchronized void methodA() {
System.out.println("ThreadName:" + Thread.currentThread().getName() + ",begin methodA, count:" + getCount());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count ++;
System.out.println("ThreadName:" + Thread.currentThread().getName() + ", count:" + getCount());
}
public void methodB() {
// 加鎖
lock.lock();
try {
System.out.println("ThreadName:" + Thread.currentThread().getName() + ",begin methodB, count:" + getCount());
Thread.sleep(3000);
count ++;
System.out.println("ThreadName:" + Thread.currentThread().getName() + ", count:" + getCount());
} catch (Exception e){
e.printStackTrace();
} finally {
// 釋放鎖
lock.unlock();
}
}
public int getCount() {
return count;
}
}
public class MyThreadA extends Thread {
private Counter counter;
public MyThreadA(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
counter.methodA();
}
}
public class MyThreadB extends Thread {
private Counter counter;
public MyThreadB(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
counter.methodB();
}
}
public class MyThreadTest {
public static void main(String[] args) {
Counter counter = new Counter();
MyThreadA threadA = new MyThreadA(counter);
threadA.start();
MyThreadB threadB = new MyThreadB(counter);
threadB.start();
}
}
看一下運行結果:
ThreadName:Thread-0,begin methodA, count:0
ThreadName:Thread-1,begin methodB, count:0
ThreadName:Thread-0, count:2
ThreadName:Thread-1, count:2
從日誌上可以看出,採用兩個線程分別採用synchronized
和ReentrantLock
兩種加鎖方式對count
進行操作,兩個線程交替執行,可以得出一個結論:synchronized
和ReentrantLock
持有的對象監視器不同。
四、Condition 基本用法
在之前的文章中,我們介紹了在synchronized
同步方法/代碼塊中,使用wait()
、notify()
和notifyAll()
可以實現線程之間的等待/通知模型。
ReentrantLock
同樣也可以,只需要藉助Condition
類即可實現,Condition
提供的await()
、signal()
、signalAll()
原理和synchronized
鎖對象的wait()
、notify()
、notifyAll()
是一致的,並且其行爲也是一樣的。
我們還是先來看一個簡單的示例。
public class Counter {
private final Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private int count;
public void await(){
// 加鎖
lock.lock();
try {
condition.await();
System.out.println("await等待結束,count:" + getCount());
} catch (Exception e){
e.printStackTrace();
} finally {
// 釋放鎖
lock.unlock();
}
}
public void signal(){
// 加鎖
lock.lock();
try {
count++;
// 喚醒某個等待線程
condition.signal();
// 喚醒所有等待線程
// condition.signalAll();
System.out.println("signal 喚醒通知完畢");
} catch (Exception e){
e.printStackTrace();
} finally {
// 釋放鎖
lock.unlock();
}
}
public int getCount() {
return count;
}
}
public class MyThreadA extends Thread {
private Counter counter;
public MyThreadA(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
counter.await();
}
}
public class MyThreadB extends Thread {
private Counter counter;
public MyThreadB(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
counter.signal();
}
}
public class MyThreadTest {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// 先啓動執行等待的線程
MyThreadA threadA = new MyThreadA(counter);
threadA.start();
Thread.sleep(3000);
// 過3秒,再啓動執行通知的線程
MyThreadB threadB = new MyThreadB(counter);
threadB.start();
}
}
看一下運行結果:
signal 通知完畢
await等待結束,count:1
從結果上看很明顯的看出,等待線程MyThreadA
先啓動,過了 3 秒之後再啓動了MyThreadB
,但是signal()
方法先執行完畢,再通知await()
方法執行,符合代碼預期。
這個例子也證明了一點:condition.await()
方法是釋放了鎖,不然signal()
方法體不會被執行。
相比wait/notify/notifyAll
的等待/通知模型,Condition
更加靈活,理由有以下幾點:
notify()
方法喚醒線程時,被通知的線程由 Java 虛擬機隨機選擇;而採用ReentrantLock
結合Condition
可以實現有選擇性地通知,這一特性在實際編程中非常實用- 一個
Lock
裏面可以創建多個Condition
實例,實現多路通知,使用多個Condition
的應用場景很常見,比如ArrayBlockingQueue
五、小結
本文主要圍繞ReentrantLock
的基本使用做了一次簡單的知識總結,如果有不正之處,請多多諒解,並歡迎批評指出。
六、參考
1、博客園 -五月的倉頡 - ReentrantLock的使用和Condition
七、寫到最後
最近無意間獲得一份阿里大佬寫的技術筆記,內容涵蓋 Spring、Spring Boot/Cloud、Dubbo、JVM、集合、多線程、JPA、MyBatis、MySQL 等技術知識。需要的小夥伴可以點擊如下鏈接獲取,資源地址:技術資料筆記。
不會有人刷到這裏還想白嫖吧?點贊對我真的非常重要!在線求贊。加個關注我會非常感激!