聊一聊比Synchronized更強(qiáng)大的同步鎖:ReentrantLock
01、背景介紹
我們介紹到了使用synchronized關(guān)鍵字可以實(shí)現(xiàn)線程同步安全的效果,以及采用wait()、notify()和notifyAll()方法,可以實(shí)現(xiàn)多個(gè)線程之間的通信協(xié)調(diào),基本可以滿足并發(fā)編程的需求。
但是采用synchronized進(jìn)行加鎖,這種鎖一般都比較重,里面的實(shí)現(xiàn)機(jī)制也非常復(fù)雜,同時(shí)獲取鎖時(shí)必須一直等待,沒有額外的嘗試機(jī)制,如果編程不當(dāng),可能就容易發(fā)生死鎖現(xiàn)象。
從 JDK 1.5 開始,引入了一個(gè)高級(jí)的處理并發(fā)的java.util.concurrent包,它提供了大量更高級(jí)的并發(fā)功能,能大大的簡化多線程程序的編寫。
比如我們今天要介紹的java.util.concurrent.locks包提供的ReentrantLock類,一個(gè)可重入的互斥鎖,它具有與使用synchronized加鎖一樣的特性,并且功能更加強(qiáng)大。
下面我們一起來學(xué)習(xí)一下ReentrantLock類的基本玩法。
02、ReentrantLock 玩法介紹
在介紹ReentrantLock之前,我們先來看一下傳統(tǒng)的使用synchronized對(duì)方法進(jìn)行加鎖的示例。
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();
// 創(chuàng)建5個(gè)線程,同時(shí)對(duì)count進(jìn)行加一操作
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
counter.add();
}
}).start();
}
// 假設(shè)休眠1秒,5個(gè)線程執(zhí)行完畢
Thread.sleep(1000);
System.out.println("count:" + counter.getCount());
}
輸出結(jié)果如下:
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;
}
}
運(yùn)行程序,結(jié)果與上面一致,可以證明:ReentrantLock具備與synchronized一樣的加鎖功能。
同時(shí),ReentrantLock還具備在指定的時(shí)間內(nèi)嘗試獲取鎖的機(jī)制,比如下面這行代碼:
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
...
} finally {
lock.unlock();
}
}
嘗試在 3 秒內(nèi)獲取鎖,如果獲取不到就返回false,程序不需要無限等待下去,這個(gè)功能在實(shí)際開發(fā)中使用非常的廣泛。
從上面的示例代碼,我們可以總結(jié)出synchronized和ReentrantLock有以下幾點(diǎn)不一樣。
- ReentrantLock需要手動(dòng)調(diào)用加鎖方法;而synchronized不需要,它采用了隱藏的加鎖方式,借助 jvm 來實(shí)現(xiàn)
- synchronized不需要考慮異常;而ReentrantLock獲取鎖之后,要在finally中正確的釋放鎖,否則會(huì)影響其它線程
- ReentrantLock擁有嘗試獲取鎖的超時(shí)機(jī)制,利用它可以避免無限等待;而synchronized不具備
- synchronized是 Java 語言層面提供的語法;而ReentrantLock是 Java 代碼實(shí)現(xiàn)的可重入鎖
因此,在并發(fā)編程中,使用ReentrantLock比直接使用synchronized更靈活、更安全,采用tryLock(long time, TimeUnit unit)方法,即使未獲取到鎖也不會(huì)導(dǎo)致死鎖。
03、ReentrantLock 和 synchronized 持有的對(duì)象監(jiān)視器是同一個(gè)嗎?
可能有的同學(xué)會(huì)發(fā)出這樣的一個(gè)問題,使用ReentrantLock進(jìn)行加鎖和使用synchronized加鎖,兩者持有的對(duì)象監(jiān)視器是同一個(gè)嗎?
下面我們一起來看一個(gè)例子。
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();
}
}
看一下運(yùn)行結(jié)果:
ThreadName:Thread-0,begin methodA, count:0
ThreadName:Thread-1,begin methodB, count:0
ThreadName:Thread-0, count:2
ThreadName:Thread-1, count:2
從日志上可以看出,采用兩個(gè)線程分別采用synchronized和ReentrantLock兩種加鎖方式對(duì)count進(jìn)行操作,兩個(gè)線程交替執(zhí)行,可以得出一個(gè)結(jié)論:synchronized和ReentrantLock持有的對(duì)象監(jiān)視器不同。
04、Condition 基本用法
在之前的文章中,我們介紹了在synchronized同步方法/代碼塊中,使用wait()、notify()和notifyAll()可以實(shí)現(xiàn)線程之間的等待/通知模型。
ReentrantLock同樣也可以,只需要借助Condition類即可實(shí)現(xiàn),Condition提供的await()、signal()、signalAll()原理和synchronized鎖對(duì)象的wait()、notify()、notifyAll()是一致的,并且其行為也是一樣的。
我們還是先來看一個(gè)簡單的示例。
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等待結(jié)束,count:" + getCount());
} catch (Exception e){
e.printStackTrace();
} finally {
// 釋放鎖
lock.unlock();
}
}
public void signal(){
// 加鎖
lock.lock();
try {
count++;
// 喚醒某個(gè)等待線程
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();
// 先啟動(dòng)執(zhí)行等待的線程
MyThreadA threadA = new MyThreadA(counter);
threadA.start();
Thread.sleep(3000);
// 過3秒,再啟動(dòng)執(zhí)行通知的線程
MyThreadB threadB = new MyThreadB(counter);
threadB.start();
}
}
看一下運(yùn)行結(jié)果:
signal 通知完畢
await等待結(jié)束,count:1
從結(jié)果上看很明顯的看出,等待線程MyThreadA先啟動(dòng),過了 3 秒之后再啟動(dòng)了MyThreadB,但是signal()方法先執(zhí)行完畢,再通知await()方法執(zhí)行,符合代碼預(yù)期。
這個(gè)例子也證明了一點(diǎn):condition.await()方法是釋放了鎖,不然signal()方法體不會(huì)被執(zhí)行。
相比wait/notify/notifyAll的等待/通知模型,Condition更加靈活,理由有以下幾點(diǎn):
- notify()方法喚醒線程時(shí),被通知的線程由 Java 虛擬機(jī)隨機(jī)選擇;而采用ReentrantLock結(jié)合Condition可以實(shí)現(xiàn)有選擇性地通知,這一特性在實(shí)際編程中非常實(shí)用
- 一個(gè)Lock里面可以創(chuàng)建多個(gè)Condition實(shí)例,實(shí)現(xiàn)多路通知,使用多個(gè)Condition的應(yīng)用場景很常見,比如ArrayBlockingQueue
05、參考
1、https://www.cnblogs.com/xrq730/p/4855155.html
2、https://www.liaoxuefeng.com/wiki/