在多線程環(huán)境中,多個(gè)進(jìn)程可以競(jìng)爭(zhēng)有限數(shù)量的資源。當(dāng)一個(gè)進(jìn)程申請(qǐng)資源時(shí),如果這時(shí)沒(méi)有可用資源,那么這個(gè)進(jìn)程進(jìn)入等待狀態(tài)。有時(shí),如果所申請(qǐng)的資源被其他等待進(jìn)程占有,那么該等待進(jìn)程有可能再也無(wú)法改變狀態(tài)。這種情況稱為死鎖
海曙網(wǎng)站制作公司哪家好,找創(chuàng)新互聯(lián)建站!從網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)站建設(shè)、微信開發(fā)、APP開發(fā)、成都響應(yīng)式網(wǎng)站建設(shè)公司等網(wǎng)站項(xiàng)目制作,到程序開發(fā),運(yùn)營(yíng)維護(hù)。創(chuàng)新互聯(lián)建站從2013年開始到現(xiàn)在10年的時(shí)間,我們擁有了豐富的建站經(jīng)驗(yàn)和運(yùn)維經(jīng)驗(yàn),來(lái)保證我們的工作的順利進(jìn)行。專注于網(wǎng)站建設(shè)就選創(chuàng)新互聯(lián)建站。
在Java中使用多線程,就會(huì)有可能導(dǎo)致死鎖問(wèn)題。死鎖會(huì)讓程序一直卡住,不再往下執(zhí)行。我們只能通過(guò)中止并重啟的方式來(lái)讓程序重新執(zhí)行。
進(jìn)程要求對(duì)所分配的資源(如打印機(jī))進(jìn)行排他性控制,即在一段時(shí)間內(nèi)某資源僅為一個(gè)進(jìn)程所占有。此時(shí)若有其他進(jìn)程請(qǐng)求該資源,則請(qǐng)求進(jìn)程只能等待。
進(jìn)程所獲得的資源在未使用完畢之前,不能被其他進(jìn)程強(qiáng)行奪走,即只能由獲得該資源的進(jìn)程自己來(lái)釋放(只能是主動(dòng)釋放)。
進(jìn)程已經(jīng)保持了至少一個(gè)資源,但又提出了新的資源請(qǐng)求,而該資源已被其他進(jìn)程占有,此時(shí)請(qǐng)求進(jìn)程被阻塞,但對(duì)自己已獲得的資源保持不放。
是指進(jìn)程發(fā)生死鎖后,必然存在一個(gè)進(jìn)程–資源之間的環(huán)形鏈,通俗講就是你等我的資源,我等你的資源,大家一直等。
線程之間形成相互等待資源的環(huán)時(shí),就會(huì)形成順序死鎖lock-ordering deadlock,多個(gè)線程試圖以不同的順序來(lái)獲取相同的鎖時(shí),容易形成順序死鎖,如果所有線程以固定的順序來(lái)獲取鎖,就不會(huì)出現(xiàn)順序死鎖問(wèn)題
經(jīng)典案例是LeftRightDeadlock,兩個(gè)方法,分別是leftRigth、rightLeft。如果一個(gè)線程調(diào)用leftRight,另一個(gè)線程調(diào)用rightLeft,且兩個(gè)線程是交替執(zhí)行的,就會(huì)發(fā)生死鎖。
public class LeftRightDeadLock {
//左邊鎖
private static Object left = new Object();
//右邊鎖
private static Object right = new Object();
/**
* 現(xiàn)持有左邊的鎖,然后獲取右邊的鎖
*/
public static void leftRigth() {
synchronized (left) {
System.out.println("leftRigth: left lock,threadId:" + Thread.currentThread().getId());
//休眠增加死鎖產(chǎn)生的概率
sleep(100);
synchronized (right) {
System.out.println("leftRigth: right lock,threadId:" + Thread.currentThread().getId());
}
}
}
/**
* 現(xiàn)持有右邊的鎖,然后獲取左邊的鎖
*/
public static void rightLeft() {
synchronized (right) {
System.out.println("rightLeft: right lock,threadId:" + Thread.currentThread().getId());
//休眠增加死鎖產(chǎn)生的概率
sleep(100);
synchronized (left) {
System.out.println("rightLeft: left lock,threadId:" + Thread.currentThread().getId());
}
}
}
/**
* 休眠
*
* @param time
*/
private static void sleep(long time) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
//創(chuàng)建一個(gè)線程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.execute(() -> leftRigth());
executorService.execute(() -> rightLeft());
executorService.shutdown();
}
}
輸出:
leftRigth: left lock,threadId:12
rightLeft: right lock,threadId:13
我們發(fā)現(xiàn),12號(hào)線程鎖住了左邊要向右邊獲取鎖,13號(hào)鎖住了右邊,要向左邊獲取鎖,因?yàn)閮蛇叾疾会尫抛约旱逆i,互不相讓,就產(chǎn)生了死鎖。
固定加鎖的順序(針對(duì)鎖順序死鎖)
只要交換下鎖的順序,讓線程來(lái)了之后先獲取同一把鎖,獲取不到就等待,等待上一個(gè)線程釋放鎖再獲取鎖。
public static void leftRigth() {
synchronized (left) {
...
synchronized (right) {
...
}
}
}
public static void rightLeft() {
synchronized (left) {
...
synchronized (right) {
...
}
}
}
由于方法入?yún)⒂赏獠總鬟f而來(lái),方法內(nèi)部雖然對(duì)兩個(gè)參數(shù)按照固定順序進(jìn)行加鎖,但是由于外部傳遞時(shí)順序的不可控,而產(chǎn)生鎖順序造成的死鎖,即動(dòng)態(tài)鎖順序死鎖。
上例告訴我們,交替的獲取鎖會(huì)導(dǎo)致死鎖,且鎖是固定的。有時(shí)候鎖的執(zhí)行順序并不那么清晰,參數(shù)導(dǎo)致不同的執(zhí)行順序。經(jīng)典案例是銀行賬戶轉(zhuǎn)賬,from賬戶向to賬戶轉(zhuǎn)賬,在轉(zhuǎn)賬之前先獲取兩個(gè)賬戶的鎖,然后開始轉(zhuǎn)賬,如果這是to賬戶向from賬戶轉(zhuǎn)賬,角色互換,也會(huì)導(dǎo)致鎖順序死鎖。
/**
* 動(dòng)態(tài)順序型死鎖
* 轉(zhuǎn)賬業(yè)務(wù)
*/
public class TransferMoneyDeadlock {
public static void transfer(Account from, Account to, int amount) {
//先鎖住轉(zhuǎn)賬的賬戶
synchronized (from) {
System.out.println("線程【" + Thread.currentThread().getId() + "】獲取【" + from.name + "】賬戶鎖成功");
//休眠增加死鎖產(chǎn)生的概率
sleep(100);
//在鎖住目標(biāo)賬戶
synchronized (to) {
System.out.println("線程【" + Thread.currentThread().getId() + "】獲取【" + to.name + "】賬戶鎖成功");
if (from.balance < amount) {
System.out.println("余額不足");
return;
} else {
from.debit(amount);
to.credit(amount);
System.out.println("線程【" + Thread.currentThread().getId() + "】從【" + from.name + "】賬戶轉(zhuǎn)賬到【" + to.name + "】賬戶【" + amount + "】元錢成功");
}
}
}
}
private static class Account {
String name;
int balance;
public Account(String name, int balance) {
this.name = name;
this.balance = balance;
}
void debit(int amount) {
this.balance = balance - amount;
}
void credit(int amount) {
this.balance = balance + amount;
}
}
/**
* 休眠
*
* @param time
*/
private static void sleep(long time) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
//創(chuàng)建線程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
//創(chuàng)建賬戶A
Account A = new Account("A", 100);
//創(chuàng)建賬戶B
Account B = new Account("B", 200);
//A -> B 的轉(zhuǎn)賬
executorService.execute(() -> transfer(A, B, 5));
//B -> A 的轉(zhuǎn)賬
executorService.execute(() -> transfer(B, A, 10));
executorService.shutdown();
}
}
輸出:
線程【12】獲取【A】賬戶鎖成功
線程【13】獲取【B】賬戶鎖成功
然后就沒(méi)有然后了,產(chǎn)生了死鎖,我們發(fā)現(xiàn) 因?yàn)閷?duì)象的調(diào)用關(guān)系,產(chǎn)生了互相鎖住資源的問(wèn)題。
根據(jù)傳入對(duì)象的hashCode硬性確定加鎖順序,消除可變性,避免死鎖。
package com.test.thread.deadlock;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 動(dòng)態(tài)順序型死鎖解決方案
*/
public class TransferMoneyDeadlock {
/**
* 監(jiān)視器,第三把鎖,為了方式HASH沖突
*/
private static Object lock = new Object();
/**
* 我們經(jīng)過(guò)上一次得失敗,明白了不能依賴參數(shù)名稱簡(jiǎn)單的確定鎖的順序,因?yàn)閰?shù)是
* 具有動(dòng)態(tài)性的,所以,我們改變一下思路,直接根據(jù)傳入對(duì)象的hashCode()大小來(lái)
* 對(duì)鎖定順序進(jìn)行排序(這里要明白的是如何排序不是關(guān)鍵,有序才是關(guān)鍵)。
*
* @param from
* @param to
* @param amount
*/
public static void transfer(Account from, Account to, int amount) {
/**
* 這里需要說(shuō)明一下為什么不使用HashCode()因?yàn)镠ashCode方法可以被重寫,
* 所以,我們無(wú)法簡(jiǎn)單的使用父類或者當(dāng)前類提供的簡(jiǎn)單的hashCode()方法,
* 所以,我們就使用系統(tǒng)提供的identityHashCode()方法,該方法保證無(wú)論
* 你是否重寫了hashCode方法,都會(huì)在虛擬機(jī)層面上調(diào)用一個(gè)名為JVM_IHashCode
* 的方法來(lái)根據(jù)對(duì)象的存儲(chǔ)地址來(lái)獲取該對(duì)象的hashCode(),HashCode如果不重寫
* 的話,其實(shí)也是通過(guò)這個(gè)虛擬機(jī)層面上的方法,JVM_IHashCode()方法實(shí)現(xiàn)的
* 這個(gè)方法是用C++實(shí)現(xiàn)的。
*/
int fromHash = System.identityHashCode(from);
int toHash = System.identityHashCode(to);
if (fromHash > toHash) {
//先鎖住轉(zhuǎn)賬的賬戶
synchronized (from) {
System.out.println("線程【" + Thread.currentThread().getId() + "】獲取【" + from.name + "】賬戶鎖成功");
//休眠增加死鎖產(chǎn)生的概率
sleep(100);
//在鎖住目標(biāo)賬戶
synchronized (to) {
System.out.println("線程【" + Thread.currentThread().getId() + "】獲取【" + to.name + "】賬戶鎖成功");
if (from.balance < amount) {
System.out.println("余額不足");
return;
} else {
from.debit(amount);
to.credit(amount);
System.out.println("線程【" + Thread.currentThread().getId() + "】從【" + from.name + "】賬戶轉(zhuǎn)賬到【" + to.name + "】賬戶【" + amount + "】元錢成功");
}
}
}
} else if (fromHash < toHash) {
//先鎖住轉(zhuǎn)賬的賬戶
synchronized (to) {
System.out.println("線程【" + Thread.currentThread().getId() + "】獲取【" + from.name + "】賬戶鎖成功");
//休眠增加死鎖產(chǎn)生的概率
sleep(100);
//在鎖住目標(biāo)賬戶
synchronized (from) {
System.out.println("線程【" + Thread.currentThread().getId() + "】獲取【" + to.name + "】賬戶鎖成功");
if (from.balance < amount) {
System.out.println("余額不足");
return;
} else {
from.debit(amount);
to.credit(amount);
System.out.println("線程【" + Thread.currentThread().getId() + "】從【" + from.name + "】賬戶轉(zhuǎn)賬到【" + to.name + "】賬戶【" + amount + "】元錢成功");
}
}
}
} else {
//如果傳入對(duì)象的Hash值相同,那就加讓加第三層鎖
synchronized (lock) {
//先鎖住轉(zhuǎn)賬的賬戶
synchronized (from) {
System.out.println("線程【" + Thread.currentThread().getId() + "】獲取【" + from.name + "】賬戶鎖成功");
//休眠增加死鎖產(chǎn)生的概率
sleep(100);
//在鎖住目標(biāo)賬戶
synchronized (to) {
System.out.println("線程【" + Thread.currentThread().getId() + "】獲取【" + to.name + "】賬戶鎖成功");
if (from.balance < amount) {
System.out.println("余額不足");
return;
} else {
from.debit(amount);
to.credit(amount);
System.out.println("線程【" + Thread.currentThread().getId() + "】從【" + from.name + "】賬戶轉(zhuǎn)賬到【" + to.name + "】賬戶【" + amount + "】元錢成功");
}
}
}
}
}
}
private static class Account {
String name;
int balance;
public Account(String name, int balance) {
this.name = name;
this.balance = balance;
}
void debit(int amount) {
this.balance = balance - amount;
}
void credit(int amount) {
this.balance = balance + amount;
}
}
/**
* 休眠
*
* @param time
*/
private static void sleep(long time) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
//創(chuàng)建線程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
//創(chuàng)建賬戶A
Account A = new Account("A", 100);
//創(chuàng)建賬戶B
Account B = new Account("B", 200);
//A -> B 的轉(zhuǎn)賬
executorService.execute(() -> transfer(A, B, 5));
//B -> A 的轉(zhuǎn)賬
executorService.execute(() -> transfer(B, A, 10));
executorService.shutdown();
}
}
輸出
線程【12】獲取【A】賬戶鎖成功
線程【12】獲取【B】賬戶鎖成功
線程【12】從【A】賬戶轉(zhuǎn)賬到【B】賬戶【5】元錢成功
線程【13】獲取【B】賬戶鎖成功
線程【13】獲取【A】賬戶鎖成功
線程【13】從【B】賬戶轉(zhuǎn)賬到【A】賬戶【10】元錢成功
在協(xié)作對(duì)象之間可能存在多個(gè)鎖獲取的情況,但是這些獲取多個(gè)鎖的操作并不像在LeftRightDeadLock或transferMoney中那么明顯,這兩個(gè)鎖并不一定必須在同一個(gè)方法中被獲取。如果在持有鎖時(shí)調(diào)用某個(gè)外部方法,那么這就需要警惕死鎖問(wèn)題,因?yàn)樵谶@個(gè)外部方法中可能會(huì)獲取其他鎖,或者阻塞時(shí)間過(guò)長(zhǎng),導(dǎo)致其他線程無(wú)法及時(shí)獲取當(dāng)前被持有的鎖。
上述兩例中,在同一個(gè)方法中獲取兩個(gè)鎖。實(shí)際上,鎖并不一定在同一方法中被獲取。經(jīng)典案例,如出租車調(diào)度系統(tǒng)。
/**
* 協(xié)作對(duì)象間的死鎖
*/
public class CoordinateDeadlock {
/**
* Taxi 類
*/
static class Taxi {
private String location;
private String destination;
private Dispatcher dispatcher;
public Taxi(Dispatcher dispatcher, String destination) {
this.dispatcher = dispatcher;
this.destination = destination;
}
public synchronized String getLocation() {
return this.location;
}
/**
* 該方法先獲取Taxi的this對(duì)象鎖后,然后調(diào)用Dispatcher類的方法時(shí),又需要獲取
* Dispatcher類的this方法。
*
* @param location
*/
public synchronized void setLocation(String location) {
this.location = location;
System.out.println(Thread.currentThread().getName() + " taxi set location:" + location);
if (this.location.equals(destination)) {
dispatcher.notifyAvailable(this);
}
}
}
/**
* 調(diào)度類
*/
static class Dispatcher {
private Set taxis;
private Set availableTaxis;
public Dispatcher() {
taxis = new HashSet();
availableTaxis = new HashSet();
}
public synchronized void notifyAvailable(Taxi taxi) {
System.out.println(Thread.currentThread().getName() + " notifyAvailable.");
availableTaxis.add(taxi);
}
/**
* 打印當(dāng)前位置:有死鎖風(fēng)險(xiǎn)
* 持有當(dāng)前鎖的時(shí)候,同時(shí)調(diào)用Taxi的getLocation這個(gè)外部方法;而這個(gè)外部方法也是需要加鎖的
* reportLocation的鎖的順序與Taxi的setLocation鎖的順序完全相反
*/
public synchronized void reportLocation() {
System.out.println(Thread.currentThread().getName() + " report location.");
for (Taxi t : taxis) {
t.getLocation();
}
}
public void addTaxi(Taxi taxi) {
taxis.add(taxi);
}
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
final Dispatcher dispatcher = new Dispatcher();
final Taxi taxi = new Taxi(dispatcher, "軟件園");
dispatcher.addTaxi(taxi);
//先獲取dispatcher鎖,然后是taxi的鎖
executorService.execute(() -> dispatcher.reportLocation());
//先獲取taxi鎖,然后是dispatcher的鎖
executorService.execute(() -> taxi.setLocation("軟件園"));
executorService.shutdown();
}
}
使用開放調(diào)用,開放調(diào)用指調(diào)用該方法不需要持有鎖。
開放調(diào)用,是指在調(diào)用某個(gè)方法時(shí)不需要持有鎖。開放調(diào)用可以避免死鎖,這種代碼更容易編寫。上述調(diào)度算法完全可以修改為開發(fā)調(diào)用,修改同步代碼塊的范圍,使其僅用于保護(hù)那些涉及共享狀態(tài)的操作,避免在同步代碼塊中執(zhí)行方法調(diào)用。修改Dispatcher的reportLocation方法:
/**
* 開放調(diào)用,不持有鎖期間進(jìn)行外部方法調(diào)用
*
* @param location
*/
public void setLocation(String location) {
synchronized (this) {
this.location = location;
}
System.out.println(Thread.currentThread().getName() + " taxi set location:" + location);
if (this.location.equals(destination)) {
dispatcher.notifyAvailable(this);
}
}
/**
* 同步塊只包含對(duì)共享狀態(tài)的操作代碼
*/
public synchronized void reportLocation() {
System.out.println(Thread.currentThread().getName() + " report location.");
Set taxisCopy;
synchronized (this) {
taxisCopy = new HashSet(taxis);
}
for (Taxi t : taxisCopy) {
t.getLocation();
}
}
本文由
傳智教育博學(xué)谷
教研團(tuán)隊(duì)發(fā)布。如果本文對(duì)您有幫助,歡迎
關(guān)注
和點(diǎn)贊
;如果您有任何建議也可留言評(píng)論
或私信
,您的支持是我堅(jiān)持創(chuàng)作的動(dòng)力。轉(zhuǎn)載請(qǐng)注明出處!