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