java中常見(jiàn)的死鎖以及解決方法代碼
在java中我們常常使用加鎖機(jī)制來(lái)確保線程安全,但是如果過(guò)度使用加鎖,則可能導(dǎo)致鎖順序死鎖。同樣,我們使用線程池和信號(hào)量來(lái)限制對(duì)資源的使用,但是這些被限制的行為可能會(huì)導(dǎo)致資源死鎖。java應(yīng)用程序無(wú)法從死鎖中恢復(fù)過(guò)來(lái),因此設(shè)計(jì)時(shí)一定要排序那些可能導(dǎo)致死鎖出現(xiàn)的條件。
1.一個(gè)最簡(jiǎn)單的死鎖案例
當(dāng)一個(gè)線程永遠(yuǎn)地持有一個(gè)鎖,并且其他線程都嘗試獲得這個(gè)鎖時(shí),那么它們將永遠(yuǎn)被阻塞。在線程A持有鎖L并想獲得鎖M的同時(shí),線程B持有鎖M并嘗試獲得鎖L,那么這兩個(gè)線程將永遠(yuǎn)地等待下去。這種就是最簡(jiǎn)答的死鎖形式(或者叫做"抱死")。
2.鎖順序死鎖
如圖:leftRight和rightLeft這兩個(gè)方法分別獲得left鎖和right鎖。如果一個(gè)線程調(diào)用了leftRight,而另一個(gè)線程調(diào)用了rightLeft,并且這兩個(gè)線程的操作是交互執(zhí)行,那么它們就會(huì)發(fā)生死鎖。
死鎖的原因就是兩個(gè)線程試圖以不同的順序來(lái)獲得相同的鎖。所以,如果所有的線程以固定的順序來(lái)獲得鎖,那么在程序中就不會(huì)出現(xiàn)鎖順序死鎖的問(wèn)題。
2.1.動(dòng)態(tài)的鎖順序死鎖
我以一個(gè)經(jīng)典的轉(zhuǎn)賬案例來(lái)進(jìn)行說(shuō)明,我們知道轉(zhuǎn)賬就是將資金從一個(gè)賬戶轉(zhuǎn)入另一個(gè)賬戶。在開(kāi)始轉(zhuǎn)賬之前,首先需要獲得這兩個(gè)賬戶對(duì)象得鎖,以確保通過(guò)原子方式來(lái)更新兩個(gè)賬戶中的余額,同時(shí)又不破壞一些不變形條件,例如 賬戶的余額不能為負(fù)數(shù)。
所以寫(xiě)出的代碼如下:
//動(dòng)態(tài)的鎖的順序死鎖 public class DynamicOrderDeadlock { public static void transferMoney(Account fromAccount,Account toAccount,int amount,int from_index,int to_index) throws Exception { System.out.println("賬戶 "+ from_index+"~和賬戶~(yú)"+to_index+" ~請(qǐng)求鎖"); synchronized (fromAccount) { System.out.println(" 賬戶 >>>"+from_index+" <<<獲得鎖"); synchronized (toAccount) { System.out.println(" 賬戶 "+from_index+" & "+to_index+"都獲得鎖"); if (fromAccount.compareTo(amount) < 0) { throw new Exception(); }else { fromAccount.debit(amount); toAccount.credit(amount); } } } } static class Account { private int balance = 100000;//這里假設(shè)每個(gè)人賬戶里面初始化的錢(qián) private final int accNo; private static final AtomicInteger sequence = new AtomicInteger(); public Account() { accNo = sequence.incrementAndGet(); } void debit(int m) throws InterruptedException { Thread.sleep(5);//模擬操作時(shí)間 balance = balance + m; } void credit(int m) throws InterruptedException { Thread.sleep(5);//模擬操作時(shí)間 balance = balance - m; } int getBalance() { return balance; } int getAccNo() { return accNo; } public int compareTo(int money) { if (balance > money) { return 1; }else if (balance < money) { return -1; }else { return 0; } } } }
public class DemonstrateDeadLock { private static final int NUM_THREADS = 5; private static final int NUM_ACCOUNTS = 5; private static final int NUM_ITERATIONS = 100000; public static void main(String[] args) { final Random rnd = new Random(); final Account[] accounts = new Account[NUM_ACCOUNTS]; for(int i = 0;i < accounts.length;i++) { accounts[i] = new Account(); } class TransferThread extends Thread{ @Override public void run() { for(int i = 0;i < NUM_ITERATIONS;i++) { int fromAcct = rnd.nextInt(NUM_ACCOUNTS); int toAcct =rnd.nextInt(NUM_ACCOUNTS); int amount = rnd.nextInt(100); try { DynamicOrderDeadlock.transferMoney(accounts[fromAcct],accounts[toAcct], amount,fromAcct,toAcct); //InduceLockOrder.transferMoney(accounts[fromAcct],accounts[toAcct], amount); //InduceLockOrder2.transferMoney(accounts[fromAcct],accounts[toAcct], amount); }catch (Exception e) { System.out.println("發(fā)生異常-------"+e); } } } } for(int i = 0;i < NUM_THREADS;i++) { new TransferThread().start(); } } }
打印結(jié)果如下:
注意:這里的結(jié)果是我把已經(jīng)執(zhí)行完的給刪除后,只剩下導(dǎo)致死鎖的請(qǐng)求.
//通過(guò)鎖順序來(lái)避免死鎖 public class InduceLockOrder { private static final Object tieLock = new Object(); public static void transferMoney(final Account fromAcct, final Account toAcct, final int amount) throws Exception { class Helper { public void transfer() throws Exception { if (fromAcct.compareTo(amount) < 0) { throw new Exception(); } else { fromAcct.debit(amount); toAcct.credit(amount); } } } int fromHash = System.identityHashCode(fromAcct); int toHash = System.identityHashCode(toAcct); if (fromHash < toHash) { synchronized (fromAcct) { synchronized (toAcct) { new Helper().transfer(); } } } else if (fromHash > toHash) { synchronized (toAcct) { synchronized (fromAcct) { new Helper().transfer(); } } } else { synchronized (tieLock) { synchronized (fromAcct) { synchronized (toAcct) { new Helper().transfer(); } } } } } static class Account { private int balance = 100000; public Account() { } void debit(int m) throws InterruptedException { Thread.sleep(5); balance = balance + m; } void credit(int m) throws InterruptedException { Thread.sleep(5); balance = balance - m; } int getBalance() { return balance; } public int compareTo(int money) { if (balance > money) { return 1; }else if (balance < money) { return -1; }else { return 0; } } } }
經(jīng)過(guò)我測(cè)試,此方案可行,不會(huì)造成死鎖。
方案二
在Account中包含一個(gè)唯一的,不可變的,值。比如說(shuō)賬號(hào)等。通過(guò)對(duì)這個(gè)值對(duì)對(duì)象進(jìn)行排序。
具體代碼如下
public class InduceLockOrder2 { public static void transferMoney(final Account fromAcct, final Account toAcct, final int amount) throws Exception { class Helper { public void transfer() throws Exception { if (fromAcct.compareTo(amount) < 0) { throw new Exception(); } else { fromAcct.debit(amount); toAcct.credit(amount); } } } int fromHash = fromAcct.getAccNo(); int toHash = toAcct.getAccNo(); if (fromHash < toHash) { synchronized (fromAcct) { synchronized (toAcct) { new Helper().transfer(); } } } else if (fromHash > toHash) { synchronized (toAcct) { synchronized (fromAcct) { new Helper().transfer(); } } } } static class Account { private int balance = 100000; private final int accNo; private static final AtomicInteger sequence = new AtomicInteger(); public Account() { accNo = sequence.incrementAndGet(); } void debit(int m) throws InterruptedException { Thread.sleep(6); balance = balance + m; } void credit(int m) throws InterruptedException { Thread.sleep(6); balance = balance - m; } int getBalance() { return balance; } int getAccNo() { return accNo; } public int compareTo(int money) { if (balance > money) { return 1; }else if (balance < money) { return -1; }else { return 0; } } } }
經(jīng)過(guò)測(cè)試此方案也可行。
2.2在協(xié)作對(duì)象之間發(fā)生的死鎖
如果在持有鎖時(shí)調(diào)用某外部的方法,那么將出現(xiàn)活躍性問(wèn)題。在這個(gè)外部方法中可能會(huì)獲取其他的鎖(這個(gè)可能產(chǎn)生死鎖),或阻塞時(shí)間過(guò)長(zhǎng),導(dǎo)致其他線程無(wú)法及時(shí)獲得當(dāng)前持有的鎖。
場(chǎng)景如下:Taxi代表出租車對(duì)象,包含當(dāng)前位置和目的地。Dispatcher代表車隊(duì)。當(dāng)一個(gè)線程收到GPS更新事件時(shí)掉用setLocation,那么它首先更新出租車的位置,然后判斷它是否到達(dá)目的地。如果已經(jīng)到達(dá),它會(huì)通知Dispatcher:它需要一個(gè)新的目的地。因?yàn)閟etLocation和notifyAvailable都是同步方法,因此掉用setLocation線程首先獲取taxi的鎖,然后在獲取Dispatcher的鎖。同樣,掉用getImage的線程首先獲取Dispatcher的鎖,再獲取每一個(gè)taxi的鎖,這兩個(gè)線程按照不同的順序來(lái)獲取鎖,因此可能導(dǎo)致死鎖。
能造成死鎖的代碼如下:
//會(huì)發(fā)生死鎖 public class CooperatingDeadLock { // 坐標(biāo)類 class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } } // 出租車類 class Taxi { private Point location, destination; private final Dispatcher dispatcher; public Taxi(Dispatcher dispatcher) { this.dispatcher = dispatcher; } public synchronized Point getLocation() { return location; } public synchronized void setLocation(Point location) { this.location = location; if (location.equals(destination)) { dispatcher.notifyAvailable(this); } } public synchronized Point getDestination() { return destination; } public synchronized void setDestination(Point destination) { this.destination = destination; } } class Dispatcher { private final Set<Taxi> taxis; private final Set<Taxi> availableTaxis; public Dispatcher() { taxis = new HashSet<>(); availableTaxis = new HashSet<>(); } public synchronized void notifyAvailable(Taxi taxi) { availableTaxis.add(taxi); } public synchronized Image getImage() { Image image = new Image(); for(Taxi t:taxis) { image.drawMarker(t.getLocation()); } return image; } } class Image{ public void drawMarker(Point p) { } } }
解決方案:使用開(kāi)放掉用。
如果再調(diào)用某個(gè)方法時(shí)不需要持有鎖,那么這種調(diào)用就被稱為開(kāi)放掉用。這種調(diào)用能有效的避免死鎖,并且易于分析線程安全。
修改后的代碼如下:
//此方案不會(huì)造成死鎖 public class CooperatingNoDeadlock { // 坐標(biāo)類 class Point { private final int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } } // 出租車類 class Taxi { private Point location, destination; private final Dispatcher dispatcher; public Taxi(Dispatcher dispatcher) { this.dispatcher = dispatcher; } public synchronized Point getLocation() { return location; } public void setLocation(Point location) { boolean reachedDestination; synchronized (this) { this.location = location; reachedDestination = location.equals(destination); } if (reachedDestination) { dispatcher.notifyAvailable(this); } } public synchronized Point getDestination() { return destination; } public synchronized void setDestination(Point destination) { this.destination = destination; } } class Dispatcher { private final Set<Taxi> taxis; private final Set<Taxi> availableTaxis; public Dispatcher() { taxis = new HashSet<>(); availableTaxis = new HashSet<>(); } public synchronized void notifyAvailable(Taxi taxi) { availableTaxis.add(taxi); } public Image getImage() { Set<Taxi> copy; synchronized (this) { copy = new HashSet<>(taxis); } Image image = new Image(); for(Taxi t:copy) { image.drawMarker(t.getLocation()); } return image; } } class Image{ public void drawMarker(Point p) { } } }
總結(jié):活躍性故障是一個(gè)非常嚴(yán)重的問(wèn)題,因?yàn)楫?dāng)出現(xiàn)活躍性故障時(shí),除了終止應(yīng)用程序之外沒(méi)有其他任何機(jī)制可以幫助從這種故障中恢復(fù)過(guò)來(lái)。最常見(jiàn)的活躍性故障就是鎖順序死鎖。在設(shè)計(jì)時(shí)應(yīng)該避免產(chǎn)生順序死鎖:確保線程在獲取多個(gè)鎖時(shí)采用一直的順序。最好的解決方案是在程序中始終使用開(kāi)放掉用。這將大大減小需要同時(shí)持有多個(gè)鎖的地方,也更容易發(fā)現(xiàn)這些地方。
以上所述是小編給大家介紹的java中常見(jiàn)的死鎖以及解決方法詳解整合,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
相關(guān)文章
Java使用Poi導(dǎo)出Excel表格方法實(shí)例
這篇文章主要給大家介紹了關(guān)于Java使用Poi導(dǎo)出Excel表格的相關(guān)資料,Java POI是一個(gè)用于操作Microsoft Office格式的Java API庫(kù),可以使用它來(lái)導(dǎo)出Excel文件,需要的朋友可以參考下2023-10-10詳解Spring Cloud Gateway 數(shù)據(jù)庫(kù)存儲(chǔ)路由信息的擴(kuò)展方案
這篇文章主要介紹了詳解Spring Cloud Gateway 數(shù)據(jù)庫(kù)存儲(chǔ)路由信息的擴(kuò)展方案,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-11-11Springboot2.x結(jié)合Mabatis3.x下Hikari連接數(shù)據(jù)庫(kù)報(bào)超時(shí)錯(cuò)誤
本文針對(duì)Springboot2.x與Mybatis3.x結(jié)合使用時(shí),Hikari連接數(shù)據(jù)庫(kù)出現(xiàn)超時(shí)錯(cuò)誤的問(wèn)題進(jìn)行了深入分析,并提供了一系列有效的解決方法,感興趣的可以了解一下2023-11-11java讀取圖片并轉(zhuǎn)化為二進(jìn)制字符串的實(shí)現(xiàn)方法
這篇文章主要介紹了java讀取圖片并轉(zhuǎn)化為二進(jìn)制字符串的實(shí)例代碼,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-09-09深入理解Java設(shè)計(jì)模式之職責(zé)鏈模式
這篇文章主要介紹了JAVA設(shè)計(jì)模式之職責(zé)鏈模式的的相關(guān)資料,文中示例代碼非常詳細(xì),供大家參考和學(xué)習(xí),感興趣的朋友可以了解2021-11-11基于java SSM springboot實(shí)現(xiàn)景區(qū)行李寄存管理系統(tǒng)
這篇文章主要介紹了基于java SSM springboot實(shí)現(xiàn)的景區(qū)行李寄存管理系統(tǒng),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-08-08SpringBoot中@Scheduled()注解以及cron表達(dá)式詳解
這篇文章主要介紹了SpringBoot中@Scheduled()注解以及cron表達(dá)式詳解,@Scheduled注解是Spring Boot提供的用于定時(shí)任務(wù)控制的注解,主要用于控制任務(wù)在某個(gè)指定時(shí)間執(zhí)行,或者每隔一段時(shí)間執(zhí)行,需要的朋友可以參考下2023-08-08