Java高并發系列——ReentrantLock
ReentrantLock重入鎖
synchronized的局限性
synchronized是java內置的關鍵字,它提供了一種獨占的加鎖方式。synchronized的獲取和釋放鎖由jvm實現,用戶不需要顯示的釋放鎖,非常方便,然而synchronized也有一定的局限性,例如:
- 當線程嘗試獲取鎖的時候,如果獲取不到鎖會一直阻塞,這個阻塞的過程,用戶無法控制。(Synchronized不可中斷的說法:只有獲取到鎖之后才能中斷,等待鎖時不可中斷。)
- 如果獲取鎖的線程進入休眠或者阻塞,除非當前線程異常,否則其他線程嘗試獲取鎖必須一直等待。(synchronized不能響應中斷?)
ReentrantLock
ReentrantLock是Lock的默認實現,在聊ReentranLock之前,我們需要先弄清楚一些概念:
- 可重入鎖:可重入鎖是指同一個線程可以多次獲得同一把鎖;ReentrantLock和關鍵字Synchronized都是可重入鎖
- 可中斷鎖:可中斷鎖是指線程在獲取鎖的過程中,是否可以響應線程中斷操作。synchronized是不可中斷的,ReentrantLock是可中斷的
- 公平鎖和非公平鎖:公平鎖是指多個線程嘗試獲取同一把鎖的時候,獲取鎖的順序按照線程到達的先后順序獲取,而不是隨機插隊的方式獲取。synchronized是非公平鎖,而ReentrantLock是兩種都可以實現,不過默認是非公平鎖。
ReentrantLock基本使用
ReentrantLock的使用過程:
- 創建鎖:ReentrantLock lock = new ReentrantLock();
- 獲取鎖:lock.lock()
- 釋放鎖:lock.unlock();
對比上面的代碼,與關鍵字synchronized相比,ReentrantLock鎖有明顯的操作過程,開發人員必須手動的指定何時加鎖,何時釋放鎖,正是因為這樣手動控制,ReentrantLock對邏輯控制的靈活度要遠遠勝于關鍵字synchronized,上面代碼需要注意lock.unlock()一定要放在finally中,否則若程序出現了異常,鎖沒有釋放,那么其他線程就再也沒有機會獲取這個鎖了。
ReentrantLock是可重入鎖
假如ReentrantLock是不可重入的鎖,那么同一個線程第2次獲取鎖的時候由于前面的鎖還未釋放而導致死鎖,程序是無法正常結束的。
- lock()方法和unlock()方法需要成對出現,鎖了幾次,也要釋放幾次,否則后面的線程無法獲取鎖了;可以將add中的unlock刪除一個事實,上面代碼運行將無法結束
- unlock()方法放在finally中執行,保證不管程序是否有異常,鎖必定會釋放
示例:
public class ReentrantLockTest {
private static int num = 0;
private static Lock lock = new ReentrantLock();
public static void add() {
lock.lock();
lock.lock();
try {
num++;
} finally {
//lock()方法和unlock()方法需要成對出現,鎖了幾次,也要釋放幾次,否則后面的線程無法獲取鎖
lock.unlock();
lock.unlock();
}
}
public static class T extends Thread {
public T(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
ReentrantLockTest.add();
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T("t1");
T t2 = new T("t2");
T t3 = new T("t3");
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println("get num =" + num);
}
}
//輸出: get num =3000
ReentrantLock實現公平鎖
在大多數情況下,鎖的申請都是非公平的。這就好比買票不排隊,上廁所不排隊。最終導致的結果是,有些人可能一直買不到票。而公平鎖,它會按照到達的先后順序獲得資源。公平鎖的一大特點是不會產生饑餓現象,只要你排隊,最終還是可以等到資源的;synchronized關鍵字默認是有jvm內部實現控制的,是非公平鎖。而ReentrantLock運行開發者自己設置鎖的公平性,可以實現公平和非公平鎖。
看一下jdk中ReentrantLock的源碼,2個構造方法:
public ReentrantLock() { sync = new NonfairSync();}
public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync();}
默認構造方法創建的是非公平鎖。
第2個構造方法,有個fair參數,當fair為true的時候創建的是公平鎖,公平鎖看起來很不錯,不過要實現公平鎖,系統內部肯定需要維護一個有序隊列,因此公平鎖的實現成本比較高,性能相對于非公平鎖來說相對低一些。因此,在默認情況下,鎖是非公平的,如果沒有特別要求,則不建議使用公平鎖。
示例:
public class ReentrantLockFairTest {
private static int num = 0;
//private static Lock lock = new ReentrantLock(false);
private static Lock lock = new ReentrantLock(true);
public static class T extends Thread {
public T(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+" got lock");
} finally {
lock.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T("t1");
T t2 = new T("t2");
T t3 = new T("t3");
t1.start();
t2.start();
t3.start();
}
}
輸出:
公平鎖:
t1 got lock
t1 got lock
t2 got lock
t2 got lock
t3 got lock
t3 got lock
非公平鎖:
t1 got lock
t3 got lock
t3 got lock
t2 got lock
t2 got lock
t1 got lock
ReentrantLock獲取鎖的過程是可中斷的——使用lockInterruptibly()和tryLock(long time, TimeUnit unit)有參方法時。
對于synchronized關鍵字,如果一個線程在等待獲取鎖,最終只有2種結果:
- 要么獲取到鎖然后繼續后面的操作
- 要么一直等待,直到其他線程釋放鎖為止
而ReentrantLock提供了另外一種可能,就是在等的獲取鎖的過程中(發起獲取鎖請求到還未獲取到鎖這段時間內)是可以被中斷的,也就是說在等待鎖的過程中,程序可以根據需要取消獲取鎖的請求。拿李云龍平安縣圍點打援來說,當平安縣城被拿下后,鬼子救援的部隊再嘗試救援已經沒有意義了,這時候要請求中斷操作。
關于獲取鎖的過程中被中斷,注意幾點:
- ReentrankLock中必須使用實例方法 lockInterruptibly()獲取鎖時,在線程調用interrupt()方法之后,才會引發 InterruptedException異常
- 線程調用interrupt()之后,線程的中斷標志會被置為true
- 觸發InterruptedException異常之后,線程的中斷標志有會被清空,即置為false
- 所以當線程調用interrupt()引發InterruptedException異常,中斷標志的變化是:false->true->false
實例:
public class InterruptTest2 {
private static ReentrantLock lock1 = new ReentrantLock();
private static ReentrantLock lock2 = new ReentrantLock();
public static class T1 extends Thread {
int lock;
public T1(String name, Integer lock) {
super(name);
this.lock = lock;
}
@Override
public void run() {
try {
if (lock == 1) {
lock1.lockInterruptibly();
TimeUnit.SECONDS.sleep(1);
lock2.lockInterruptibly();
} else {
lock2.lockInterruptibly();
TimeUnit.SECONDS.sleep(1);
lock1.lockInterruptibly();
}
} catch (InterruptedException e) {
//線程發送中斷信號觸發InterruptedException異常之后,中斷標志將被清空。
System.out.println(this.getName() + "中斷標志:" + this.isInterrupted());
e.printStackTrace();
} finally {
//ReentrantLock自有的方法,多態實現的Lock不能用
if (lock1.isHeldByCurrentThread()) {
lock1.unlock();
}
if (lock2.isHeldByCurrentThread()) {
lock2.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
T1 t1 = new T1("thread1", 1);
T1 t2 = new T1("thread2", 2);
t1.start();
t2.start();
TimeUnit.SECONDS.sleep(1000);
//不加interrupt()通過jstack查看線程堆棧信息,發現2個線程死鎖了
//"thread2":
// waiting for ownable synchronizer 0x000000076b782028, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
// which is held by "thread1"
//"thread1":
// waiting for ownable synchronizer 0x000000076b782058, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
// which is held by "thread2"
t1.interrupt();
}
}
ReentrantLock鎖申請等待限時
ReentrantLock剛好提供了這樣功能,給我們提供了獲取鎖限時等待的方法 tryLock()
,可以選擇傳入時間參數,表示等待指定的時間,無參則表示立即返回鎖申請的結果:true表示獲取鎖成功,false表示獲取鎖失敗。
tryLock無參方法——tryLock()是立即響應的,中間不會有阻塞。
看一下源碼中tryLock方法:
public boolean tryLock()
tryLock有參方法
該方法在指定的時間內不管是否可以獲取鎖,都會返回結果,返回true,表示獲取鎖成功,返回false表示獲取失敗。 此方法在執行的過程中,如果調用了線程的中斷interrupt()方法,會觸發InterruptedException異常。
可以明確設置獲取鎖的超時時間:
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
關于tryLock()方法和tryLock(long timeout, TimeUnit unit)方法,說明一下:
- 都會返回boolean值,結果表示獲取鎖是否成功。
- tryLock()方法,不管是否獲取成功,都會立即返回;而有參的tryLock方法會嘗試在指定的時間內去獲取鎖,中間會阻塞的現象,在指定的時間之后會不管是否能夠獲取鎖都會返回結果。
- tryLock()方法不會響應線程的中斷方法;而有參的tryLock方法會響應線程的中斷方法,而出發
InterruptedException
異常,這個從2個方法的聲明上可以可以看出來。
ReentrantLock其他常用的方法
- isHeldByCurrentThread:實例方法,判斷當前線程是否持有ReentrantLock的鎖,上面代碼中有使用過。
獲取鎖的4種方法對比
獲取鎖的方法 | 是否立即響應(不會阻塞) | 是否響應中斷 |
---|---|---|
lock() | × | × |
lockInterruptibly() | × | √ |
tryLock() | √ | × |
tryLock(long timeout, TimeUnit unit) | × | √ |
實例:
public class ReentrantLockTest1 {
private static ReentrantLock lock1 = new ReentrantLock();
public static class T extends Thread {
public T(String name) {
super(name);
}
@Override
public void run() {
try {
System.out.println(this.getName()+"嘗試獲取鎖");
if (lock1.tryLock(2,TimeUnit.SECONDS)){
System.out.println(this.getName()+"獲取鎖成功");
TimeUnit.SECONDS.sleep(3);
}else {
System.out.println(this.getName()+"獲取鎖失敗");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(lock1.isHeldByCurrentThread()){
lock1.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T("t1");
T t2 = new T("t2");
t1.start();
t2.start();
}
}
輸出:
lock1.tryLock()
t1嘗試獲取鎖
t1獲取鎖成功
t2嘗試獲取鎖
t2獲取鎖失敗
lock1.tryLock(2,TimeUnit.SECONDS)
t1嘗試獲取鎖
t2嘗試獲取鎖
t1獲取鎖成功
t2獲取鎖失敗
總結
- ReentrantLock可以實現公平鎖和非公平鎖
- ReentrantLock默認實現的是非公平鎖
- ReentrantLock的獲取鎖和釋放鎖必須成對出現,鎖了幾次,也要釋放幾次
- 釋放鎖的操作必須放在finally中執行
- lockInterruptibly()實例方法可以響應線程的中斷方法,調用線程的interrupt()方法時,lockInterruptibly()方法會觸發
InterruptedException
異常 - 關于
InterruptedException
異常說一下,看到方法聲明上帶有throwsInterruptedException
,表示該方法可以相應線程中斷,調用線程的interrupt()方法時,這些方法會觸發InterruptedException
異常,觸發InterruptedException時,線程的中斷中斷狀態會被清除。所以如果程序由于調用interrupt()
方法而觸發InterruptedException
異常,線程的標志由默認的false變為ture,然后又變為false - 實例方法tryLock()獲會嘗試獲取鎖,會立即返回,返回值表示是否獲取成功
- 實例方法tryLock(long timeout, TimeUnit unit)會在指定的時間內嘗試獲取鎖,指定的時間內是否能夠獲取鎖,都會返回,返回值表示是否獲取鎖成功,該方法會響應線程的中斷
疑問
Q:可中斷鎖:可中斷鎖時線程在獲取鎖的過程中,是否可以相應線程中斷操作。為什么synchronized是不可中斷的,ReentrantLock是可中斷的?
首先,只有獲取到鎖之后才能中斷,等待鎖時不可中斷。
查看Thread.interrupt()
源碼發現,這里面的操作只是做了修改一個中斷狀態值為true,并沒有顯式聲明拋出InterruptedException
異常。因此:
- 若線程被中斷前,如果該線程處于非阻塞狀態(未調用過
wait
,sleep
,join
方法),那么該線程的中斷狀態將被設為true, 除此之外,不會發生任何事。 - 若線程被中斷前,該線程處于阻塞狀態(調用了
wait
,sleep
,join
方法),那么該線程將會立即從阻塞狀態中退出,并拋出一個InterruptedException
異常,同時,該線程的中斷狀態被設為false, 除此之外,不會發生任何事。
所以說,Synchronized
鎖此時為輕量級鎖或重量級鎖,此時等待線程是在自旋運行或者已經是重量級鎖導致的阻塞狀態了(非調用了wait
,sleep
,join
等方法的阻塞),只把中斷狀態設為true,沒有拋出異常真正中斷。
而ReentrantLock.lockInterruptibly()
首次嘗試獲取鎖之前就會判斷是否應該中斷,如果沒有獲取到鎖,在自旋等待的時候也會繼續判斷中斷狀態。(代碼里會判斷中斷狀態,所有會響應中斷。)
JUC中的Condition對象
Condition使用簡介——實現等待/通知機制
注意:在使用使用Condition.await()方法時,需要先獲取Condition對象關聯的ReentrantLock的鎖;就像使用Object.wait()時必須在synchronized同步代碼塊內。
從整體上來看Object的wait和notify/notify是與對象監視器配合完成線程間的等待/通知機制,而Condition與Lock配合完成等待通知機制,前者是java底層級別的,后者是語言級別的,具有更高的可控制性和擴展性。兩者除了在使用方式上不同外,在功能特性上還是有很多的不同:
- Condition能夠支持不響應中斷,而通過使用Object方式不支持
- Condition能夠支持多個等待隊列(new 多個Condition對象),而Object方式只能支持一個
- Condition能夠支持超時時間的設置,而Object不支持
Condition由ReentrantLock對象創建,并且可以同時創建多個,Condition接口在使用前必須先調用ReentrantLock的lock()方法獲得鎖,之后調用Condition接口的await()將釋放鎖,并且在該Condition上等待,直到有其他線程調用Condition的signal()方法喚醒線程,使用方式和wait()、notify()類似。
需要注意的時,當一個線程被signal()方法喚醒線程時,它第一個動作是去獲取同步鎖,注意這一點,而這把鎖目前在調用signal()方法喚醒他的線程上,必須等其釋放鎖后才能得到爭搶鎖的機會。
實例:
public class ConditionTest {
private static ReentrantLock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
public static void main(String[] args) {
T1 t1 = new T1("TT1");
T2 t2 = new T2("TT2");
t1.start();
t2.start();
}
static class T1 extends Thread {
public T1(String name) {
super(name);
}
@Override
public void run() {
lock.lock();
System.out.println(this.getName() + " start");
try {
System.out.println(this.getName() + " wait");
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
System.out.println(this.getName() + " end");
}
}
static class T2 extends Thread {
public T2(String name) {
super(name);
}
@Override
public void run() {
lock.lock();
System.out.println(this.getName() + " start");
System.out.println(this.getName() + " signal");
condition.signal();
System.out.println(this.getName() + " end");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.getName() + " end,2 second later");
}
}
}
輸出:
TT1 start
TT1 wait
TT2 start
TT2 signal
TT2 end
TT2 end,2 second later
Condition常用方法
和Object中wait類似的方法
- void await() throws InterruptedException:當前線程進入等待狀態,如果在等待狀態中被中斷會拋出被中斷異常;
- long awaitNanos(long nanosTimeout):當前線程進入等待狀態直到被通知,中斷或者超時;
- boolean await(long time, TimeUnit unit) throws InterruptedException:同第二種,支持自定義時間單位,false:表示方法超時之后自動返回的,true:表示等待還未超時時,await方法就返回了(超時之前,被其他線程喚醒了)
- boolean awaitUntil(Date deadline) throws InterruptedException:當前線程進入等待狀態直到被通知,中斷或者到了某個時間
- void awaitUninterruptibly();:當前線程進入等待狀態,不會響應線程中斷操作,只能通過喚醒的方式讓線程繼續
和Object的notify/notifyAll類似的方法
- void signal():喚醒一個等待在condition上的線程,將該線程從等待隊列中轉移到同步隊列中,如果在同步隊列中能夠競爭到Lock則可以從等待方法中返回。
- void signalAll():與1的區別在于能夠喚醒所有等待在condition上的線程
Condition.await()過程中被打斷
調用condition.await()之后,線程進入阻塞中,調用t1.interrupt(),給t1線程發送中斷信號,await()方法內部會檢測到線程中斷信號,然后觸發 InterruptedException
異常,線程中斷標志被清除。從輸出結果中可以看出,線程t1中斷標志的變換過程:false->true->false
await(long time, TimeUnit unit)超時之后自動返回
t1線程等待2秒之后,自動返回繼續執行,最后await方法返回false,await返回false表示超時之后自動返回
await(long time, TimeUnit unit)超時之前被喚醒
t1線程中調用 condition.await(5,TimeUnit.SECONDS);
方法會釋放鎖,等待5秒,主線程休眠1秒,然后獲取鎖,之后調用signal()方法喚醒t1,輸出結果中發現await后過了1秒(1、3行輸出結果的時間差),await方法就返回了,并且返回值是true。true表示await方法超時之前被其他線程喚醒了。
long awaitNanos(long nanosTimeout)超時返回
t1調用await方法等待5秒超時返回,返回結果為負數,表示超時之后返回的。
//awaitNanos參數為納秒,可以調用TimeUnit中的一些方法將時間轉換為納秒。
long nanos = TimeUnit.SECONDS.toNanos(2);
waitNanos(long nanosTimeout)超時之前被喚醒
t1中調用await休眠5秒,主線程休眠1秒之后,調用signal()喚醒線程t1,await方法返回正數,表示返回時距離超時時間還有多久,將近4秒,返回正數表示,線程在超時之前被喚醒了。
其他幾個有參的await方法和無參的await方法一樣,線程調用interrupt()方法時,這些方法都會觸發InterruptedException異常,并且線程的中斷標志會被清除。
同一個鎖支持創建多個Condition
使用兩個Condition來實現一個阻塞隊列的例子:
public class MyBlockingQueue<E> {
//阻塞隊列最大容量
private int size;
//隊列底層實現
private LinkedList<E> list = new LinkedList<>();
private static Lock lock = new ReentrantLock();
//隊列滿時的等待條件
private static Condition fullFlag = lock.newCondition();
//隊列空時的等待條件
private static Condition emptyFlag = lock.newCondition();
public MyBlockingQueue(int size) {
this.size = size;
}
public void enqueue(E e) throws InterruptedException {
lock.lock();
try {
//隊列已滿,在fullFlag條件上等待
while (list.size() == size) {
fullFlag.await();
}
//入隊:加入鏈表末尾
list.add(e);
System.out.println("生產了" + e);
//通知在emptyFlag條件上等待的線程
emptyFlag.signal();
} finally {
lock.unlock();
}
}
public E dequeue() throws InterruptedException {
lock.lock();
try {
while (list.size() == 0) {
emptyFlag.await();
}
E e = list.removeFirst();
System.out.println("消費了" + e);
//通知在fullFlag條件上等待的線程
fullFlag.signal();
return e;
} finally {
lock.unlock();
}
}
/**
* 創建了一個阻塞隊列,大小為3,隊列滿的時候,會被阻塞,等待其他線程去消費,隊列中的元素被消費之后,會喚醒生產者,生產數據進入隊列。上面代碼將隊列大小置為1,可以實現同步阻塞隊列,生產1個元素之后,生產者會被阻塞,待消費者消費隊列中的元素之后,生產者才能繼續工作。
* @param args
*/
public static void main(String[] args) {
MyBlockingQueue<Integer> queue = new MyBlockingQueue<>(1);
for (int i = 0; i < 10; i++) {
int finalI = i;
Thread producer = new Thread(() -> {
try {
queue.enqueue(finalI);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producer.start();
}
for (int i = 0; i < 10; i++) {
Thread consumer = new Thread(() -> {
try {
queue.dequeue();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
consumer.start();
}
}
}
輸出:
生產了0
消費了0
生產了1
消費了1
。。。。
生產了9
消費了9
Object的監視器方法與Condition接口的對比
注意同步隊列和等待隊列的區別,同步隊列表示在競爭一把鎖的隊列中,是處于阻塞或運行狀態的隊列。
而等待隊列是指被置為等待、超時等待狀態的線程,這些是沒有競爭鎖的權限的,處于等待被喚醒的狀態中。
對比項 | Object 監視器方法 | Condition |
---|---|---|
前置條件 | 獲取對象的鎖 | 調用Lock.lock獲取鎖,調用Lock.newCondition()獲取Condition對象 |
調用方式 | 直接調用,如:object.wait() | 直接調用,如:condition.await() |
等待隊列個數 | 一個 | 多個,使用多個condition實現 |
當前線程釋放鎖并進入等待狀態 | 支持 | 支持 |
當前線程釋放鎖進入等待狀態中不響應中斷 | 不支持 | 支持 |
當前線程釋放鎖并進入超時等待狀態 | 支持 | 支持 |
當前線程釋放鎖并進入等待狀態到將來某個時間 | 不支持 | 支持 |
喚醒等待隊列中的一個線程 | 支持 | 支持 |
喚醒等待隊列中的全部線程 | 支持 | 支持 |
總結
- 使用condition的步驟:創建condition對象,獲取鎖,然后調用condition的方法
- 一個ReentrantLock支持創建多個condition對象
- void await() throws InterruptedException;方法會釋放鎖,讓當前線程等待,支持喚醒,支持線程中斷
- void awaitUninterruptibly();方法會釋放鎖,讓當前線程等待,支持喚醒,不支持線程中斷
- long awaitNanos(longnanosTimeout)throws InterruptedException;參數為納秒,此方法會釋放鎖,讓當前線程等待,支持喚醒,支持中斷。超時之后返回的,結果為負數;超時之前被喚醒返回的,結果為正數(表示返回時距離超時時間相差的納秒數)
- boolean await (longtime,TimeUnitunit)throws InterruptedException;方法會釋放鎖,讓當前線程等待,支持喚醒,支持中斷。超時之后返回的,結果為false;超時之前被喚醒返回的,結果為true
- boolean awaitUntil(Datedeadline)throws InterruptedException;參數表示超時的截止時間點,方法會釋放鎖,讓當前線程等待,支持喚醒,支持中斷。超時之后返回的,結果為false;超時之前被喚醒返回的,結果為true
- void signal();會喚醒一個等待中的線程,然后被喚醒的線程會被加入同步隊列,去嘗試獲取鎖
- void signalAll();會喚醒所有等待中的線程,將所有等待中的線程加入同步隊列,然后去嘗試獲取鎖
疑問:
Q:Condition能夠支持超時時間的設置,而Object不支持。Object不是有wait(long timeout)超時時間設置么?