java中常見的死鎖以及解決方法代碼
在java中我們常常使用加鎖機(jī)制來確保線程安全,但是如果過度使用加鎖,則可能導(dǎo)致鎖順序死鎖。同樣,我們使用線程池和信號量來限制對資源的使用,但是這些被限制的行為可能會導(dǎo)致資源死鎖。java應(yīng)用程序無法從死鎖中恢復(fù)過來,因此設(shè)計時一定要排序那些可能導(dǎo)致死鎖出現(xiàn)的條件。
1.一個最簡單的死鎖案例
當(dāng)一個線程永遠(yuǎn)地持有一個鎖,并且其他線程都嘗試獲得這個鎖時,那么它們將永遠(yuǎn)被阻塞。在線程A持有鎖L并想獲得鎖M的同時,線程B持有鎖M并嘗試獲得鎖L,那么這兩個線程將永遠(yuǎn)地等待下去。這種就是最簡答的死鎖形式(或者叫做"抱死")。
2.鎖順序死鎖

如圖:leftRight和rightLeft這兩個方法分別獲得left鎖和right鎖。如果一個線程調(diào)用了leftRight,而另一個線程調(diào)用了rightLeft,并且這兩個線程的操作是交互執(zhí)行,那么它們就會發(fā)生死鎖。
死鎖的原因就是兩個線程試圖以不同的順序來獲得相同的鎖。所以,如果所有的線程以固定的順序來獲得鎖,那么在程序中就不會出現(xiàn)鎖順序死鎖的問題。
2.1.動態(tài)的鎖順序死鎖
我以一個經(jīng)典的轉(zhuǎn)賬案例來進(jìn)行說明,我們知道轉(zhuǎn)賬就是將資金從一個賬戶轉(zhuǎn)入另一個賬戶。在開始轉(zhuǎn)賬之前,首先需要獲得這兩個賬戶對象得鎖,以確保通過原子方式來更新兩個賬戶中的余額,同時又不破壞一些不變形條件,例如 賬戶的余額不能為負(fù)數(shù)。
所以寫出的代碼如下:
//動態(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+"~和賬戶~"+to_index+" ~請求鎖");
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è)每個人賬戶里面初始化的錢
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);//模擬操作時間
balance = balance + m;
}
void credit(int m) throws InterruptedException {
Thread.sleep(5);//模擬操作時間
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)致死鎖的請求.

//通過鎖順序來避免死鎖
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)過我測試,此方案可行,不會造成死鎖。
方案二
在Account中包含一個唯一的,不可變的,值。比如說賬號等。通過對這個值對對象進(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)過測試此方案也可行。
2.2在協(xié)作對象之間發(fā)生的死鎖
如果在持有鎖時調(diào)用某外部的方法,那么將出現(xiàn)活躍性問題。在這個外部方法中可能會獲取其他的鎖(這個可能產(chǎn)生死鎖),或阻塞時間過長,導(dǎo)致其他線程無法及時獲得當(dāng)前持有的鎖。
場景如下:Taxi代表出租車對象,包含當(dāng)前位置和目的地。Dispatcher代表車隊。當(dāng)一個線程收到GPS更新事件時掉用setLocation,那么它首先更新出租車的位置,然后判斷它是否到達(dá)目的地。如果已經(jīng)到達(dá),它會通知Dispatcher:它需要一個新的目的地。因為setLocation和notifyAvailable都是同步方法,因此掉用setLocation線程首先獲取taxi的鎖,然后在獲取Dispatcher的鎖。同樣,掉用getImage的線程首先獲取Dispatcher的鎖,再獲取每一個taxi的鎖,這兩個線程按照不同的順序來獲取鎖,因此可能導(dǎo)致死鎖。
能造成死鎖的代碼如下:
//會發(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) {
}
}
}
解決方案:使用開放掉用。
如果再調(diào)用某個方法時不需要持有鎖,那么這種調(diào)用就被稱為開放掉用。這種調(diào)用能有效的避免死鎖,并且易于分析線程安全。
修改后的代碼如下:
//此方案不會造成死鎖
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é):活躍性故障是一個非常嚴(yán)重的問題,因為當(dāng)出現(xiàn)活躍性故障時,除了終止應(yīng)用程序之外沒有其他任何機(jī)制可以幫助從這種故障中恢復(fù)過來。最常見的活躍性故障就是鎖順序死鎖。在設(shè)計時應(yīng)該避免產(chǎn)生順序死鎖:確保線程在獲取多個鎖時采用一直的順序。最好的解決方案是在程序中始終使用開放掉用。這將大大減小需要同時持有多個鎖的地方,也更容易發(fā)現(xiàn)這些地方。
以上所述是小編給大家介紹的java中常見的死鎖以及解決方法詳解整合,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
相關(guān)文章
JVM運(yùn)行時數(shù)據(jù)區(qū)原理解析
這篇文章主要介紹了JVM運(yùn)行時數(shù)據(jù)區(qū)原理解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-08-08
SpringBoot修改子模塊Module的jdk版本的方法 附修改原因
這篇文章主要介紹了SpringBoot修改子模塊Module的jdk版本的方法 附修改原因,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-04-04
淺析SpringCloud Alibaba-Nacos 作為注冊中心示例代碼
這篇文章主要介紹了SpringCloud Alibaba-Nacos 作為注冊中心示例代碼,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-10-10
Java easyexcel導(dǎo)出報內(nèi)存溢出的問題解決
在Java開發(fā)時,使用EasyExcel處理大數(shù)據(jù)量導(dǎo)出可能遇到內(nèi)存溢出問題,本文深入分析了內(nèi)存溢出的原因,并提出了優(yōu)化策略,感興趣的可以了解一下2024-10-10
基于SpringBoot + Redis實現(xiàn)密碼暴力破解防護(hù)
在現(xiàn)代應(yīng)用程序中,保護(hù)用戶密碼的安全性是至關(guān)重要的,密碼暴力破解是指通過嘗試多個密碼組合來非法獲取用戶賬戶的密碼,為了保護(hù)用戶密碼不被暴力破解,我們可以使用Spring Boot和Redis來實現(xiàn)一些防護(hù)措施,本文將介紹如何利用這些技術(shù)來防止密碼暴力破解攻擊2023-06-06

