我們知道,并發(fā)領(lǐng)域中有兩大核心問題:互斥與同步問題,Java在1.5版本之前,是提供了synchronized來實(shí)現(xiàn)的。synchronized是內(nèi)置鎖,雖然在大部分情況下它都能很好的工作,但是依然還是會存在一些局限性,除了當(dāng)時1.5版本的性能問題外(1.6版本后,synchronized的性能已經(jīng)得到了很大的優(yōu)化),還有如下兩個問題:
- 無法解決死鎖問題
- 最多使用一個條件變量
所以針對這些問題,Doug Lea在并發(fā)包中增加了兩個接口Lock和Condition來解決這兩個問題,所以今天就說說這兩個接口是如何解決synchronized中的這兩個問題的。
一. Lock接口
1.1 介紹
在我們分析Lock接口是如何解決死鎖問題之前,我們先看看死鎖是如何產(chǎn)生的。死鎖的產(chǎn)生需要滿足下面四個條件:
- 互斥 :共享資源同一時間只能被一個線程占用
- 不可搶占 :其他線程不能強(qiáng)行占有另一個線程的資源
- 占有且等待 :線程在等待其他資源時,不釋放自己已占有的資源
- 循環(huán)等待 :線程1和線程2互相占有對方的資源并相互等待
所以,我們只需要破壞上面條件中的任意一個,即可打破死鎖。但需要注意的是,互斥條件是不能破壞的,因?yàn)槭褂面i的目的就是為了互斥。所以Lock接口通過破壞掉 "不可搶占"這個條件來解決死鎖,具體如下:
- 非阻塞獲取鎖 :嘗試獲取鎖,如果失敗了就立刻返回失敗,這樣就可以釋放已經(jīng)持有的其他鎖
- 響應(yīng)中斷 :如果發(fā)生死鎖后,此線程被其他線程中斷,則會釋放鎖,解除死鎖
- 支持超時 :一段時間內(nèi)獲取不到鎖,就返回失敗,這樣就可以釋放之前已經(jīng)持有的鎖
接下來我們具體看看接口代碼吧。
1.2 源碼解讀
public interface Lock {
/**
阻塞獲取鎖,不響應(yīng)中斷,如果獲取不到,則當(dāng)前線程將進(jìn)入休眠狀態(tài),直到獲得鎖為止。
*/
void lock();
/**
阻塞獲取鎖,響應(yīng)中斷,如果出現(xiàn)以下兩種情況將拋出異常
1.調(diào)用該方法時,此線程中斷標(biāo)志位被設(shè)置為true
2.獲取鎖的過程中此線程被中斷,并且獲取鎖的實(shí)現(xiàn)會響應(yīng)中斷
*/
void lockInterruptibly() throws InterruptedException;
/**
非阻塞獲取鎖,不管成功還是失敗,都會立刻返回結(jié)果,成功了返回true,失敗了返回false
*/
boolean tryLock();
/**
帶超時時間且響應(yīng)中斷的獲取鎖,如果獲取鎖成功,則返回true,獲取不到則會休眠,直到下面三個條件滿足
1.當(dāng)前線程獲取到鎖
2.其他線程中斷了當(dāng)前線程,并且獲取鎖的實(shí)現(xiàn)支持中斷
3.設(shè)置的超時事件到了
而拋出異常的情況與lockInterruptibly一致
當(dāng)異常拋出后中斷標(biāo)志位會被清除,且超時時間到了,當(dāng)前線程還沒有獲得鎖,則會直接返回false
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
沒啥好說,只有擁有鎖的線程才能釋放鎖
*/
void unlock();
/**
返回綁定到此Lock實(shí)例的新Condition實(shí)例。
在等待該條件之前,該鎖必須由當(dāng)前線程持有。調(diào)用Condition.await()會在等待之前自動釋放鎖,并在等待返回之前重新獲取該鎖。
我們再下一小節(jié)再詳細(xì)說說Condition接口
*/
Condition newCondition();
}
還需要額外注意的一點(diǎn),使用synchronized作為鎖時,我們是不需要考慮釋放鎖的,但Lock是屬于顯示鎖,是需要我們手動釋放鎖的。我們一般在finally塊中調(diào)用lock.unlock()手動釋放鎖,具體形式如下:
Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}
我們最后通過一張圖來總結(jié)下Lock接口:
二. Condition接口
2.1 介紹
針對synchronized最多只能使用一個條件變量的問題,Condition接口提供了解決方案。但是為什么多個條件變量就比一個條件變量好呢?我們先來看看synchronized使用一個條件變量時會有什么弊端。
一個synchronized內(nèi)置鎖只對應(yīng)一個等待容器(wait set),當(dāng)線程調(diào)用wait方法時,會把當(dāng)前線程放入到同一個等待容器中,當(dāng)我們需要根據(jù)某些特定的條件來喚醒符合條件的線程時,我們只能先從等待容器里喚醒一個線程后,再看是否符合條件。如果不符合條件,則需要將此線程繼續(xù)wait,然后再去等待容器中獲取下一個線程再判斷是否滿足條件。這樣會導(dǎo)致許多無意義的cpu開銷。
我們可以看到Lock接口中有個newCondition()的方法:
Condition newCondition();
通過這個方法,一個鎖可以建立多個Conditiion,每個Condtition都有一個容器來保存相應(yīng)的等待線程,拿到鎖的線程根據(jù)特定的條件喚醒對應(yīng)的線程時,只需要去喚醒對應(yīng)的Contition內(nèi)置容器中的線程即可,這樣就可以減少無意義的CPU開銷。然后我們具體看看Condition接口的源碼。
2.2 源碼解讀
public interface Condition {
/**
使當(dāng)前線程等待,并響應(yīng)中斷。當(dāng)當(dāng)前線程進(jìn)入休眠狀態(tài)后,如果發(fā)生以下四種情況將會被喚醒:
1.其他一些線程對此條件調(diào)用signal方法,而當(dāng)前線程恰好被選擇為要喚醒的線程;
2.其他一些線程對此條件調(diào)用signalAll方法
3.其他一些線程中斷當(dāng)前線程,并支持中斷線程掛起
4.發(fā)生“虛假喚醒”。
*/
void await() throws InterruptedException;
/**
使當(dāng)前線程等待,并不響應(yīng)中斷。只有以下三種情況才會被喚醒
1.其他一些線程對此條件調(diào)用signal方法,而當(dāng)前線程恰好被選擇為要喚醒的線程;
2.其他一些線程對此條件調(diào)用signalAll方法
3.發(fā)生“虛假喚醒”。
*/
void awaitUninterruptibly();
/**
使當(dāng)前線程等待,響應(yīng)中斷,且可以指定超時事件。發(fā)生以下五種情況之一將會被喚醒:
1.其他一些線程為此條件調(diào)用signal方法,而當(dāng)前線程恰好被選擇為要喚醒的線程;
2.其他一些線程為此條件調(diào)用signalAll方法;
3.其他一些線程中斷當(dāng)前線程,并且支持中斷線程掛起;
4.經(jīng)過指定的等待時間;
5.發(fā)生“虛假喚醒”。
*/
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,則說明是因?yàn)槌瑫r返回的,否則返回true。為什么增加返回值?為了就是幫助我們弄清楚方法返回的原因。
四. 阿里多線程考題
最后我們通過實(shí)現(xiàn)了Lock和Condition接口能力的ReentrantLock類來解決阿里多線程面試題。
題目是使用三個線程循環(huán)打印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();
}
}
五. 總結(jié)
Lock與Condition接口就說完了,最后再總結(jié)一下:
針對synchronized內(nèi)置鎖無法解決死鎖、只有一個條件變量等問題,Doug Lea在Java并發(fā)包中增加了Lock和Condition接口來解決。對于死鎖問題,Lock接口增加了超時、響應(yīng)中斷、非阻塞三種方式來獲取鎖,從而避免了死鎖。針對一個條件變量問題,Condtition接口通過一把鎖可以創(chuàng)建多個條件變量的方式來解決。
-
接口
+關(guān)注
關(guān)注
33文章
9001瀏覽量
153725 -
JAVA
+關(guān)注
關(guān)注
20文章
2989瀏覽量
109608 -
Lock
+關(guān)注
關(guān)注
0文章
11瀏覽量
7912 -
線程
+關(guān)注
關(guān)注
0文章
508瀏覽量
20208
發(fā)布評論請先 登錄
Linux下線程間通訊---讀寫鎖和條件變量

評論