關於死鎖,你知道多少?
本文就什麼是死鎖?怎麼找到死鎖?怎麼解決死鎖?怎麼避免死鎖等問題展開分析,通過大量的代碼和案例演示向大家描述死鎖的前世今生。
死鎖是什麼,有什麼危害?
定義
- 併發情況下,當兩個(或多個)線程(或進程)相互持有對方所需要的資源,又不主動釋放,導致所有人都無法繼續前進,程序無限阻塞,就是死鎖
兩個線程:
多個線程:
危害
-
死鎖的影響在不同系統中是不一樣的,這取決於系統對死鎖的處理能力
- 數據庫中:檢測並放棄事務
- JVM中:無法自動處理
-
死鎖的機率不高但是危害大
- 一旦發生,多是高併發場景,影響用戶多
- 整個系統崩潰,子系統崩潰,性能降低
- 壓力測試無法找到所有的死鎖
寫一個死鎖的例子
案例一:一定會死鎖
第一個線程拿到鎖o1後等待500毫秒,這段時間第二個線程可以拿到鎖o2
然後線程1等待鎖o2,線程2等待鎖o1
造成程序無限阻塞的現象
代碼演示如下:
/**
* 〈必定發生死鎖的現象〉
*
* @author Chkl
* @create 2020/3/9
* @since 1.0.0
*/
public class MustDeadLock implements Runnable {
int flag = 1;
static Object o1 = new Object();
static Object o2 = new Object();
public static void main(String[] args) {
MustDeadLock r1 = new MustDeadLock();
MustDeadLock r2 = new MustDeadLock();
r1.flag = 1;
r2.flag = 0;
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"開始了,flag = " + flag);
if (flag == 1) {
synchronized (o1){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
System.out.println("線程1拿到兩把鎖");
}
}
} else if (flag == 0) {
synchronized (o2){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
System.out.println("線程2拿到兩把鎖");
}
}
}
}
}
案例二:兩個賬戶轉賬
模擬兩個賬戶進行轉賬
-
如果線程獲得一個鎖後等待500毫秒,會出現和案例一的死鎖現象
-
如果線程獲得一個鎖之後不等待500毫秒,只有很小的機率纔會發生死鎖,通常測試都會正常執行。
代碼演示如下:
/**
* 〈轉賬時出現死鎖〉
* 一旦註釋打開,發生死鎖
*
* @author Chkl
* @create 2020/3/9
* @since 1.0.0
*/
public class TransferMoney implements Runnable {
int flag = 1;
static Account a = new Account(500);
static Account b = new Account(500);
public static void main(String[] args) throws InterruptedException {
TransferMoney r1 = new TransferMoney();
TransferMoney r2 = new TransferMoney();
r1.flag = 1;
r2.flag = 0;
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
;
t2.start();
t1.join();
t2.join();
System.out.println("a的餘額" + a.balance);
System.out.println("b的餘額" + b.balance);
}
@Override
public void run() {
if (flag == 1) {
transferMoney(a, b, 200);
}
if (flag == 0) {
transferMoney(b, a, 200);
}
}
public static void transferMoney(Account from, Account to, int amount) {
synchronized (from) {
//如果休眠500毫秒,那麼另一個線程就會拿到to鎖,造成相互等待的死鎖現象
// try {
// Thread.sleep(500);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
synchronized (to) {
if (from.balance - amount < 0) {
System.out.println("餘額不足,轉賬失敗!");
} else {
from.balance -= amount;
to.balance += amount;
System.out.println("成功轉賬" + amount + "元");
}
}
}
}
//賬戶對象,擁有屬性balance
static class Account {
int balance;
public Account(int balance) {
this.balance = balance;
}
}
}
案例三:多人多次轉賬
如果兩個鎖之間不進行等待,很難發生死鎖
爲了驗證不等待也會發生死鎖,並且死鎖的發生是具有傳遞性的(而不是僅有少數鎖住其他正常運行的),下面我們來完成多人多次轉賬案例
設置500個賬戶,每個線程進行操作,並且每個線程轉賬100000次。每次轉賬的賬戶和金額都是隨機產生的
演示代碼如下:
/**
* 〈模擬多人隨機轉賬〉
*
* @author Chkl
* @create 2020/3/9
* @since 1.0.0
*/
public class MultiTransferMoney {
//賬戶數
private static final int NUM_ACCOUNTS = 500;
//賬戶金額
private static final int NUM_MONEY = 1000;
//每人轉賬次數
private static final int NUM_ITERATIONS = 100000;
//同時轉賬人數
private static final int NUM_THREADS = 5000;
public static void main(String[] args) {
Random random = new Random();
Account[] accounts = new Account[NUM_ACCOUNTS];
for (int i = 0; i < accounts.length; i++) {
accounts[i] = new Account(NUM_MONEY);
}
class TransferThread extends Thread {
@Override
public void run() {
for (int i = 0; i < NUM_ITERATIONS; i++) {
int fromAcc = random.nextInt(NUM_ACCOUNTS);
int toAcc = random.nextInt(NUM_ACCOUNTS);
int amount = random.nextInt(NUM_MONEY);
transferMoney(accounts[fromAcc], accounts[toAcc], amount);
}
System.out.println("運行結束!");
}
}
for (int i = 0;i<NUM_THREADS;i++){
new TransferThread().start();
}
}
public static void transferMoney(Account from, Account to, int amount) {
synchronized (from) {
synchronized (to) {
if (from.balance - amount < 0) {
System.out.println("餘額不足,轉賬失敗!");
} else {
from.balance -= amount;
to.balance += amount;
System.out.println("成功轉賬" + amount + "元");
}
}
}
}
//賬戶對象,擁有屬性balance
static class Account {
int balance;
public Account(int balance) {
this.balance = balance;
}
}
}
運行一段時間之後,死鎖的現象就出現了,控制檯沒有輸出“運行結束!”,並且進程也未結束。
驗證了依然會發生死鎖,並且死鎖具有傳遞性,並不是只有一兩個線程死鎖,而是所有線程都會被鎖死
發生死鎖必須滿足哪些條件
四個條件缺一不可:
- 互斥條件
一個資源每一次只能被一個進程或者線程同時使用
- 請求與保持條件
一個線程去請求一把鎖,同時它自身還保持一把鎖
請求的時候發生阻塞了,保持的鎖也不釋放
- 不剝奪條件
沒有外界條件來剝奪一個鎖的擁有
- 循環等待條件
各個鎖之間存在相互等待的情況,構成環
如何定位死鎖
-
jstack
- 用命令行找到Java的pid(不同操作系統不同,詳細去百度吧)
- 執行
${JAVA_HOME}/bin/jstack pid
,查找死鎖的信息
-
ThreadMXBean
在代碼中獲取是否發生死鎖,如果發生了就打印出信息
在線程啓動後,休眠一段時間等待進入死鎖,然後進行檢驗並打印
//等1000毫秒,等它進入死鎖
Thread.sleep(1000);
ThreadMXBean threadMXBean =
ManagementFactory.getThreadMXBean();
long[] deadlockedThreads =
threadMXBean.findDeadlockedThreads();
//判斷是否有死鎖現象
if (deadlockedThreads != null && deadlockedThreads.length > 0) {
for (int i = 0; i < deadlockedThreads.length; i++) {
ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);
System.out.println("發現死鎖:"+threadInfo.getThreadName());
}
}
在多人多次轉賬的案例中進行檢查,運行結果如下
有哪些解決死鎖問題的策略?
線上發生死鎖怎麼辦
-
保存案發現場後立刻重啓服務器
-
暫時保證線上服務的安全,然後在利用剛纔保存的信息,排查死鎖,修改代碼,重新發版
常見修復策略
-
避免策略
- 思路:避免相反的獲取鎖的順序
- 演示:將之前的兩個賬戶轉賬的代碼進行修改,將transferMoney方法代碼修改如下
每次加鎖前判斷兩個鎖的hash值,如果兩個hash值不相等,都是先獲取hash值小的鎖,再獲取hash值大的鎖;如果發送hash衝突,就再加一把鎖鎖住加鎖的過程。保證無論什麼順序進行轉賬,都不會發生死鎖
public static void transferMoney(Account from, Account to, int amount) {
class Helper {
public void transfer() {
if (from.balance - amount < 0) {
System.out.println("餘額不足,轉賬失敗!");
} else {
from.balance -= amount;
to.balance += amount;
System.out.println("成功轉賬" + amount + "元");
}
}
}
//獲取對象的hash值
int fromHash = System.identityHashCode(from);
int toHash = System.identityHashCode(to);
//通過hash大小的比較,保證獲取鎖的順序是一定的
//如果兩個賬戶相互轉賬,都是先加hash值小的鎖,保證了兩次加鎖的順序一致,就不會有死鎖了
if (fromHash < toHash) {
synchronized (from) {
synchronized (to) {
new Helper().transfer();
}
}
} else if (fromHash > toHash) {
synchronized (to) {
synchronized (from) {
new Helper().transfer();
}
}
//hash衝突發生了
} else {
synchronized (lock) {
synchronized (from) {
synchronized (to) {
new Helper().transfer();
}
}
}
}
}
- 檢測與恢復策略
- 允許發生死鎖
- 每次調用鎖都記錄在有向圖中
- 定期檢查“鎖的調用鏈路圖”中是否存在環路
- 一旦發生死鎖,調用死鎖恢復機制
- 線程終止
逐個終止線程,直到死鎖解除,順序如下:- 優先級(前臺交互還是後臺處理)
- 已佔用資源和還需要的資源
- 已運行時間
- 資源搶佔
- 發出去的鎖收回來,讓線程回退幾步
- 缺點:可能同一個線程一直被搶佔,造成飢餓
- 線程終止
- 鴕鳥策略
如果死鎖發送的機率非常低,那麼我們就直接忽略它,知道死鎖發送的時候,再人工修復
哲學家就餐問題
問題描述
假設有五位哲學家圍坐在一張圓形餐桌旁,做以下兩件事情之一:喫飯,或者思考。喫東西的時候,他們就停止思考,思考的時候也停止喫東西。餐桌中間有一大碗麪,每兩個哲學家之間有筷子。吃麪需要兩支筷子,所以假設哲學家必須用兩隻筷子喫東西。他們只能使用自己左右手邊的那兩隻筷子。
就餐流程
- 先拿起左手的筷子
- 然後拿起右手的筷子
- 如果筷子被人使用了,那就等別人用完
- 喫完後,把筷子放回原位
代碼演示
/**
* 〈演示哲學家就餐問題導致的死鎖〉
*
* @author Chkl
* @create 2020/3/9
* @since 1.0.0
*/
public class DiningPhilosophers {
public static class Philosopher implements Runnable {
private Object leftChopstick;
private Object rightChopstick;
public Philosopher(Object leftChopstick, Object rightChopstick) {
this.leftChopstick = leftChopstick;
this.rightChopstick = rightChopstick;
}
@Override
public void run() {
try {
while (true) {
doAction("Think");
synchronized (leftChopstick) {
doAction("picked up left chopstick");
synchronized (rightChopstick) {
doAction("picked up right chopstick");
doAction("put down right chopstick");
}
doAction("put down left chopstick");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private static void doAction(String action) throws InterruptedException {
//打印操作
System.out.println(Thread.currentThread().getName() + " " + action);
//隨機休息
Thread.sleep((long) Math.random() * 100);
}
public static void main(String[] args) {
//定義哲學家
Philosopher[] philosophers = new Philosopher[5];
//定義筷子
Object[] chopticks = new Object[philosophers.length];
//初始化筷子
for (int i = 0; i < chopticks.length; i++) {
chopticks[i] = new Object();
}
//初始化哲學家
for (int i = 0; i < philosophers.length; i++) {
Object leftChopstick = chopticks[i % philosophers.length];
Object rightChopstick = chopticks[(i + 1) % philosophers.length];
philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
new Thread(philosophers[i], "哲學家" + (i + 1)+"號 ").start();
}
}
}
可能的一種結果:
每個哲學家都拿起來左邊的筷子,然後都在等待右邊的筷子,進入循環等待的死鎖現象
多種解決方案
- 服務員檢查(避免策略)
由服務員進行判斷分配,如果發現可能會發生死鎖,不允許就餐 - 改變一個哲學家拿叉子的順序(避免策略)
改變其中一個拿的順序,破壞環路 - 餐票(避免策略)
喫飯必須拿餐票,餐票一共只有4張,喫完了回收 - 領導調節(檢測與恢復策略)
定時檢查,如果發生死鎖,隨機剝奪一個的筷子
改變一個哲學家拿叉子的順序的實現
只需要修改哲學家初始化代碼,將最後一個哲學家的拿筷子順序進行交換,將代碼
philosophers[i] = new Philosopher(rightChopstick, leftChopstick);
替換成
if (i == philosophers.length - 1) {
philosophers[i] = new Philosopher(rightChopstick, leftChopstick);
} else {
philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
}
工程中如何避免死鎖
- 設置超時時間,超時發警報
- Lock的
tryLock(long timeout,TimeUnit unit)
- synchronized不具備嘗試鎖的能力
- 造成超時的可能性很多,發生了死鎖,死循環,線程執行慢
- Lock的
代碼演示:
/**
* 〈用trylock來避免死鎖〉
*
* @author Chkl
* @create 2020/3/9
* @since 1.0.0
*/
public class TryLockDeadLock implements Runnable {
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
int flag = 1;
public static void main(String[] args) {
TryLockDeadLock r1 = new TryLockDeadLock();
TryLockDeadLock r2 = new TryLockDeadLock();
r1.flag = 1;
r2.flag = 0;
new Thread(r1).start();
new Thread(r2).start();
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (flag == 1) {
try {
//嘗試鎖,超時時間800毫秒
if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
//隨機休眠下,造成每次不一樣
Thread.sleep(new Random().nextInt(1000));
System.out.println("線程1成功獲取鎖1");
if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
System.out.println("線程1成功獲取鎖2");
System.out.println("線程1成功獲取兩把鎖了");
lock2.unlock();
lock1.unlock();
break;
} else {
System.out.println("線程1獲取鎖2失敗,已重試");
lock1.unlock();
//隨機休眠下,造成每次不一樣
Thread.sleep(new Random().nextInt(1000));
}
} else {
System.out.println("線程1獲取鎖1失敗,已重試");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (flag == 0) {
try {
//嘗試鎖,超時時間3000毫秒
if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {
//隨機休眠下,造成每次不一樣
Thread.sleep(new Random().nextInt(1000));
System.out.println("線程2成功獲取鎖2");
if (lock1.tryLock(3000, TimeUnit.MILLISECONDS)) {
System.out.println("線程2成功獲取鎖1");
System.out.println("線程2成功獲取兩把鎖了");
lock1.unlock();
lock2.unlock();
break;
} else {
System.out.println("線程2獲取鎖2失敗,已重試");
lock2.unlock();
//隨機休眠下,造成每次不一樣
Thread.sleep(new Random().nextInt(1000));
}
} else {
System.out.println("線程2獲取鎖1失敗,已重試");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
一次運行結果如下:
雖然互斥的拿到了鎖,但是獲取超時後自動釋放了,解決了死鎖的情況
- 多使用併發類而不是自己設計的類
- ConcurrentHashMap、ConcurrentLinkedQueue、AtomicBoolean等
- Java.util.concurrent.atomic中的方法
- 多用併發集合少用同步集合
- 降低鎖的使用粒度:使用不同的鎖而不是一個鎖
- 如果能用同步代碼塊,就不用同步方法:自己指定鎖的對象
- 新建線程的時候最好起個有意義的名字,方便排查
- 避免鎖的嵌套實現
- 分配資源前先看看能不能收回來:銀行家算法
- 儘量不要幾個功能使用同一個鎖:專鎖專用
線程活性故障
常見的線程活性故障包括死鎖,活鎖與線程飢餓
- 活鎖:任務或者執行者沒有被阻塞,由於某些條件沒有滿足,導致一直重複嘗試,失敗,嘗試,失敗。
- 飢餓:一個或者多個線程因爲種種原因無法獲得所需要的資源,導致一直無法執行的狀態,在非公平調度模式下,會出現。
- 活鎖與死鎖的區別在於活鎖並不是嘗試一次不能獲取鎖就阻塞了,而是動態的一直嘗試獲取鎖,並且有可能解開
- Java 中導致飢餓的原因:
- 高優先級線程吞噬所有的低優先級線程的 CPU 時間。
- 線程被永久堵塞在一個等待進入同步塊的狀態,因爲其他線程總是能在它之前持續地對該同步塊進行訪問。
- 線程在等待一個本身也處於永久等待完成的對象(比如調用這個對象的 wait 方法),因爲其他線程總是被持續地獲得喚醒