我們知道,并發領域中有兩大核心問題:互斥與同步問題,Java在1.5版本之前,是提供了synchronized來實現的。synchronized是內置鎖,雖然在大部分情況下它都能很好的工作,但是依然還是會存在一些局限性,除了當時1.5版本的性能問題外(1.6版本后,synchronized的性能已經得到了很大的優化),還有如下兩個問題:
- 無法解決死鎖問題
- 最多使用一個條件變量
所以針對這些問題,Doug Lea在并發包中增加了兩個接口Lock和Condition來解決這兩個問題,所以今天就說說這兩個接口是如何解決synchronized中的這兩個問題的。
一. Lock接口
1.1 介紹
在我們分析Lock接口是如何解決死鎖問題之前,我們先看看死鎖是如何產生的。死鎖的產生需要滿足下面四個條件:
- 互斥 :共享資源同一時間只能被一個線程占用
- 不可搶占 :其他線程不能強行占有另一個線程的資源
- 占有且等待 :線程在等待其他資源時,不釋放自己已占有的資源
- 循環等待 :線程1和線程2互相占有對方的資源并相互等待
所以,我們只需要破壞上面條件中的任意一個,即可打破死鎖。但需要注意的是,互斥條件是不能破壞的,因為使用鎖的目的就是為了互斥。所以Lock接口通過破壞掉 "不可搶占"這個條件來解決死鎖,具體如下:
- 非阻塞獲取鎖 :嘗試獲取鎖,如果失敗了就立刻返回失敗,這樣就可以釋放已經持有的其他鎖
- 響應中斷 :如果發生死鎖后,此線程被其他線程中斷,則會釋放鎖,解除死鎖
- 支持超時 :一段時間內獲取不到鎖,就返回失敗,這樣就可以釋放之前已經持有的鎖
接下來我們具體看看接口代碼吧。
1.2 源碼解讀
public interface Lock {
/**
阻塞獲取鎖,不響應中斷,如果獲取不到,則當前線程將進入休眠狀態,直到獲得鎖為止。
*/
void lock();
/**
阻塞獲取鎖,響應中斷,如果出現以下兩種情況將拋出異常
1.調用該方法時,此線程中斷標志位被設置為true
2.獲取鎖的過程中此線程被中斷,并且獲取鎖的實現會響應中斷
*/
void lockInterruptibly() throws InterruptedException;
/**
非阻塞獲取鎖,不管成功還是失敗,都會立刻返回結果,成功了返回true,失敗了返回false
*/
boolean tryLock();
/**
帶超時時間且響應中斷的獲取鎖,如果獲取鎖成功,則返回true,獲取不到則會休眠,直到下面三個條件滿足
1.當前線程獲取到鎖
2.其他線程中斷了當前線程,并且獲取鎖的實現支持中斷
3.設置的超時事件到了
而拋出異常的情況與lockInterruptibly一致
當異常拋出后中斷標志位會被清除,且超時時間到了,當前線程還沒有獲得鎖,則會直接返回false
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
沒啥好說,只有擁有鎖的線程才能釋放鎖
*/
void unlock();
/**
返回綁定到此Lock實例的新Condition實例。
在等待該條件之前,該鎖必須由當前線程持有。調用Condition.await()會在等待之前自動釋放鎖,并在等待返回之前重新獲取該鎖。
我們再下一小節再詳細說說Condition接口
*/
Condition newCondition();
}
還需要額外注意的一點,使用synchronized作為鎖時,我們是不需要考慮釋放鎖的,但Lock是屬于顯示鎖,是需要我們手動釋放鎖的。我們一般在finally塊中調用lock.unlock()手動釋放鎖,具體形式如下:
Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}
我們最后通過一張圖來總結下Lock接口:
二. Condition接口
2.1 介紹
針對synchronized最多只能使用一個條件變量的問題,Condition接口提供了解決方案。但是為什么多個條件變量就比一個條件變量好呢?我們先來看看synchronized使用一個條件變量時會有什么弊端。
一個synchronized內置鎖只對應一個等待容器(wait set),當線程調用wait方法時,會把當前線程放入到同一個等待容器中,當我們需要根據某些特定的條件來喚醒符合條件的線程時,我們只能先從等待容器里喚醒一個線程后,再看是否符合條件。如果不符合條件,則需要將此線程繼續wait,然后再去等待容器中獲取下一個線程再判斷是否滿足條件。這樣會導致許多無意義的cpu開銷。
我們可以看到Lock接口中有個newCondition()的方法:
Condition newCondition();
通過這個方法,一個鎖可以建立多個Conditiion,每個Condtition都有一個容器來保存相應的等待線程,拿到鎖的線程根據特定的條件喚醒對應的線程時,只需要去喚醒對應的Contition內置容器中的線程即可,這樣就可以減少無意義的CPU開銷。然后我們具體看看Condition接口的源碼。
2.2 源碼解讀
public interface Condition {
/**
使當前線程等待,并響應中斷。當當前線程進入休眠狀態后,如果發生以下四種情況將會被喚醒:
1.其他一些線程對此條件調用signal方法,而當前線程恰好被選擇為要喚醒的線程;
2.其他一些線程對此條件調用signalAll方法
3.其他一些線程中斷當前線程,并支持中斷線程掛起
4.發生“虛假喚醒”。
*/
void await() throws InterruptedException;
/**
使當前線程等待,并不響應中斷。只有以下三種情況才會被喚醒
1.其他一些線程對此條件調用signal方法,而當前線程恰好被選擇為要喚醒的線程;
2.其他一些線程對此條件調用signalAll方法
3.發生“虛假喚醒”。
*/
void awaitUninterruptibly();
/**
使當前線程等待,響應中斷,且可以指定超時事件。發生以下五種情況之一將會被喚醒:
1.其他一些線程為此條件調用signal方法,而當前線程恰好被選擇為要喚醒的線程;
2.其他一些線程為此條件調用signalAll方法;
3.其他一些線程中斷當前線程,并且支持中斷線程掛起;
4.經過指定的等待時間;
5.發生“虛假喚醒”。
*/
long awaitNanos(long nanosTimeout) throws InterruptedException;
/**
與awaitNanos類似,時間單位不同
*/
boolean await(long time, TimeUnit unit) throws InterruptedException;
/**
與awaitNanos類似,只不過超時時間是截止時間
*/
boolean awaitUntil(Date deadline) throws InterruptedException;
/**
喚醒一個等待線程
*/
void signal();
/**
喚醒所有等待線程
*/
void signalAll();
}
需要注意的是,Object類的等待方法是沒有返回值的,但Condtition類中的部分等待方法是有返回值的。awaitNanos(long nanosTimeout)返回了剩余等待的時間;await(long time, TimeUnit unit)返回boolean值,如果返回false,則說明是因為超時返回的,否則返回true。為什么增加返回值?為了就是幫助我們弄清楚方法返回的原因。
四. 阿里多線程考題
最后我們通過實現了Lock和Condition接口能力的ReentrantLock類來解決阿里多線程面試題。
題目是使用三個線程循環打印ABC,一共打印50次。我們直接上答案:
public class Test {
int count = 0;
Lock lock = new ReentrantLock();
Condition conditionA = lock.newCondition();
Condition conditionB = lock.newCondition();
Condition conditionC = lock.newCondition();
public void printA() {
while (count < 50) {
try {
// 加鎖
lock.lock();
// 打印A
System.out.println("A");
count ++;
// 喚醒打印B的線程
conditionB.signal();
// 將自己放入ConditionA的容器中,等待其他線程的喚醒
conditionA.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 釋放鎖
lock.unlock();
}
}
}
public void printB() {
while (count < 50) {
try {
// 加鎖
lock.lock();
// 打印B
System.out.println("B");
count ++;
// 喚醒打印C的線程
conditionC.signal();
// 將自己放入ConditionB的容器中,等待其他線程的喚醒
conditionB.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 釋放鎖
lock.unlock();
}
}
}
public void printC() {
while (count < 50) {
try {
// 加鎖
lock.lock();
// 打印B
System.out.println("C");
count ++;
// 喚醒打印A的線程
conditionA.signal();
// 將自己放入ConditionC的容器中,等待其他線程的喚醒
conditionC.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
Test test = new Test();
// 建立打印ABC的三個線程
Thread theadA = new Thread(() - > {
test.printA();
});
Thread theadB = new Thread(() - > {
test.printB();
});
Thread theadC = new Thread(() - > {
test.printC();
});
// 啟動線程
theadA.start();
theadB.start();
theadC.start();
}
}
五. 總結
Lock與Condition接口就說完了,最后再總結一下:
針對synchronized內置鎖無法解決死鎖、只有一個條件變量等問題,Doug Lea在Java并發包中增加了Lock和Condition接口來解決。對于死鎖問題,Lock接口增加了超時、響應中斷、非阻塞三種方式來獲取鎖,從而避免了死鎖。針對一個條件變量問題,Condtition接口通過一把鎖可以創建多個條件變量的方式來解決。
-
接口
+關注
關注
33文章
8634瀏覽量
151370 -
JAVA
+關注
關注
19文章
2971瀏覽量
104848 -
Lock
+關注
關注
0文章
10瀏覽量
7776 -
線程
+關注
關注
0文章
505瀏覽量
19705
發布評論請先 登錄
相關推薦
評論