案例場(chǎng)景
例如賬戶A 轉(zhuǎn)賬戶B琼娘、賬戶C 轉(zhuǎn)賬戶D這兩個(gè)轉(zhuǎn)賬操作。
class Account {
private int balance;
// 轉(zhuǎn)賬
void transfer(Account target, int amt){
// 鎖定轉(zhuǎn)出賬戶
synchronized(this){ ①
// 鎖定轉(zhuǎn)入賬戶
synchronized(target){ ②
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
}
}
這種方式采用了細(xì)粒度鎖赁濒。使用細(xì)粒度鎖可以提高并行度轨奄,是性能優(yōu)化的一個(gè)重要手段。
但是會(huì)出現(xiàn)一個(gè)新的問題就是死鎖拒炎。
上面轉(zhuǎn)賬的代碼是怎么發(fā)生死鎖的呢挪拟?我們假設(shè)線程T1執(zhí)行賬戶A轉(zhuǎn)賬戶B的操作,賬戶A.transfer(賬戶B)击你;同時(shí)線程T2執(zhí)行賬戶B轉(zhuǎn)賬戶A的操作玉组,賬戶B.transfer(賬戶A)。當(dāng)T1和T2同時(shí)執(zhí)行完①處的代碼時(shí)丁侄,T1獲得了賬戶A的鎖(對(duì)于T1惯雳,this是賬戶A),而T2獲得了賬戶B的鎖(對(duì)于T2鸿摇,this是賬戶B)石景。之后T1和T2在執(zhí)行②處的代碼時(shí),T1試圖獲取賬戶B的鎖時(shí)拙吉,發(fā)現(xiàn)賬戶B已經(jīng)被鎖定(被T2鎖定)潮孽,所以T1開始等待;T2則試圖獲取賬戶A的鎖時(shí)筷黔,發(fā)現(xiàn)賬戶A已經(jīng)被鎖定(被T1鎖定)往史,所以T2也開始等待。于是T1和T2會(huì)無(wú)期限地等待下去佛舱,也就是我們所說的死鎖了椎例。
如何預(yù)防死鎖
產(chǎn)生死鎖的四個(gè)必要條件:
- 互斥,共享資源X和Y只能被一個(gè)線程占用请祖;
- 占有且等待订歪,線程T1已經(jīng)取得共享資源X,在等待共享資源Y的時(shí)候损拢,不釋放共享資源X陌粹;
- 不可搶占,其他線程不能強(qiáng)行搶占線程T1占有的資源;
- 循環(huán)等待掏秩,線程T1等待線程T2占有的資源或舞,線程T2等待線程T1占有的資源,就是循環(huán)等待蒙幻。
其中映凳,互斥這個(gè)條件我們沒有辦法破壞,因?yàn)槲覀冇面i為的就是互斥邮破。不過其他三個(gè)條件都是有辦法破壞掉的诈豌,到底如何做呢?
- 破壞占用且等待條件
可以一次性申請(qǐng)所有資源抒和,新增一個(gè)賬本管理員的概念矫渔,兩個(gè)資源只能同時(shí)給一個(gè)線程。這個(gè)管理員只能是一個(gè)單例摧莽,因?yàn)橹挥幸粋€(gè)管理員庙洼。
class Allocator {
private List<Object> als =
new ArrayList<>();
// 一次性申請(qǐng)所有資源
synchronized boolean apply(
Object from, Object to){
if(als.contains(from) ||
als.contains(to)){
return false;
} else {
als.add(from);
als.add(to);
}
return true;
}
// 歸還資源
synchronized void free(
Object from, Object to){
als.remove(from);
als.remove(to);
}
}
class Account {
// actr應(yīng)該為單例
private Allocator actr;
private int balance;
// 轉(zhuǎn)賬
void transfer(Account target, int amt){
// 一次性申請(qǐng)轉(zhuǎn)出賬戶和轉(zhuǎn)入賬戶,直到成功
while(!actr.apply(this, target))
镊辕;
try{
// 鎖定轉(zhuǎn)出賬戶
synchronized(this){
// 鎖定轉(zhuǎn)入賬戶
synchronized(target){
if (this.balance > amt){
this.balance -= amt;
target.balance += amt;
}
}
}
} finally {
actr.free(this, target)
}
}
}
- 破壞不可搶占條件
核心是要能夠主動(dòng)釋放它占有的資源油够,這一點(diǎn)synchronized是做不到的。原因是synchronized申請(qǐng)資源的時(shí)候征懈,如果申請(qǐng)不到石咬,線程直接進(jìn)入阻塞狀態(tài)了,而線程進(jìn)入阻塞狀態(tài)卖哎,啥都干不了鬼悠,也釋放不了線程已經(jīng)占有的資源。
lock可以后續(xù)補(bǔ)充亏娜。
- 破壞循環(huán)等待條件
用“等待-通知”機(jī)制優(yōu)化循環(huán)等待image
為什么說是曾經(jīng)滿足過呢厦章?因?yàn)閚otify()只能保證在通知時(shí)間點(diǎn),條件是滿足的照藻。而被通知線程的執(zhí)行時(shí)間點(diǎn)和通知的時(shí)間點(diǎn)基本上不會(huì)重合,所以當(dāng)線程執(zhí)行的時(shí)候汗侵,很可能條件已經(jīng)不滿足了(保不齊有其他線程插隊(duì))幸缕。被通知的線程要想重新執(zhí)行,仍然需要獲取到互斥鎖(因?yàn)樵?jīng)獲取的鎖在調(diào)用wait()時(shí)已經(jīng)釋放了)
notify()是會(huì)隨機(jī)地通知等待隊(duì)列中的一個(gè)線程晰韵,而notifyAll()會(huì)通知等待隊(duì)列中的所有線程发乔。
class Allocator {
private List<Object> als;
// 一次性申請(qǐng)所有資源
synchronized void apply(
Object from, Object to){
// 經(jīng)典寫法
while(als.contains(from) ||
als.contains(to)){
try{
wait();
}catch(Exception e){
}
}
als.add(from);
als.add(to);
}
// 歸還資源
synchronized void free(
Object from, Object to){
als.remove(from);
als.remove(to);
notifyAll();
}
}