死鎖
死鎖的概述
死鎖發生在併發中,並且互不相讓。
描述:當兩個或者多個線程(或者進程)互相持有對方所佔有的資源,又不主動釋放自己的資源,導致線程陷入阻塞,即爲死鎖。多個線程如果存在環形的鎖依賴關係,可能導致死鎖。
例子:有兩個人見面分別向對方鞠躬,然而出於紳士風度,兩人都不想早於對方起身。
死鎖的影響
數據庫事務發生死鎖時,數據庫會強行終止事務。但JVM無法自動處理死鎖。陷入死鎖的線程可能非常重要,不能放棄鎖,因此由程序員處理。
死鎖發生的機率不高,但是很大。一般發生在高併發場景,影響用戶多;根據死鎖在系統中位置不同,可能導致系統崩潰、性能降低等。
壓力測試無法找出所有潛在的死鎖。例如,線程等待鎖被調起是隨機的;短時間持有鎖,是爲了降低鎖的競爭程度,但卻增加了測試中找出潛在死鎖的難度。
一段必然死鎖的代碼–代碼演示
public class MustDeadLock implements Runnable {
private int flag;
public MustDeadLock(int flag) {
this.flag = flag;
}
private static final Object resourceA = new Object();
private static final Object resourceB = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new MustDeadLock(0));
Thread thread2 = new Thread(new MustDeadLock(1));
thread1.start();
thread2.start();
}
@Override
public void run() {
if (flag == 0) {
synchronized (resourceA) {
System.out.println(Thread.currentThread().getName() + "獲取A鎖");
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resourceB) {
System.out.println(Thread.currentThread().getName() + "獲取B鎖");
}
}
} else {
synchronized (resourceB) {
System.out.println(Thread.currentThread().getName() + "獲取B鎖");
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resourceA) {
System.out.println(Thread.currentThread().getName() + "獲取A鎖");
}
}
}
}
}
Thread-1獲取B鎖
Thread-0獲取A鎖
Process finished with exit code -1 / 130
控制檯正常終止結束信號是0。
代碼演示:轉賬
轉賬需要兩把鎖:轉出賬戶和轉入賬戶,兩者都不能同時轉入轉出。獲取兩把鎖成功,並且餘額大於0,則扣除轉出賬戶,增加轉入賬戶餘額,是原子操作。互相轉錢時可能導致死鎖。
死鎖的4個必要條件
- 互斥條件:資源同時只有一個線程佔用。
- 請求與保持條件:線程請求被其他線程佔用的資源,而不釋放自己佔用的資源。
- 不可剝奪條件:線程佔用的資源在使用完之前不能被其他線程搶佔。
- 循環等待條件:發生死鎖時,必然存在線程資源依賴的環形鏈。
四個條件是必要條件,必須同時滿足纔會發生死鎖。
定位死鎖
jstack精確定位死鎖
使用jps命令定位線程PID
> jps
25604 MustDeadLock
使用jstack打印死鎖信息
> jstack -l 25604
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-HSjqYKvc-1583594286763)(931BFB9548734896AD717D643BD3713E)]
jstack無法應對複雜死鎖,但是可以用來分析棧信息。
ThreadMXBean檢查死鎖
// 使用ThreadMXBean檢測死鎖
Thread.sleep(1000);
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadLockThreads = threadMXBean.findDeadlockedThreads();
if (deadLockThreads != null && deadLockThreads.length != 0) {
for (long deadLockThread : deadLockThreads) {
ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadLockThread);
System.out.println("死鎖線程:" + threadInfo.getThreadName());
}
}
死鎖修復策略
平時開發需要提前預防死鎖,如果線上出現死鎖,很難做到無損失的解決。因此要保存好堆棧信息,然後立刻重啓服務器,避免影響用戶體驗。
死鎖的修復策略有以下幾種:
- 預防與避免策略:轉賬換序方案、哲學家就餐的換手方案。
- 檢查與恢復策略:一段時間檢查是否有死鎖,如果有,剝奪資源,打破死鎖。
- 鴕鳥策略:死鎖發生概率很低,直接忽略;等到死鎖發生時,人工修復。
預防策略–轉賬換序方案【代碼演示】
思想:避免相反獲取鎖的順序。
public class TransferMoneyNotDeadLock implements Runnable {
private int flag;
public TransferMoneyNotDeadLock(int flag) {
this.flag = flag;
}
/**
* 兩個賬戶鎖
*/
private static Account account1 = new Account(500);
private static Account account2 = new Account(500);
/**
* 額外的鎖,用於hashcode相同時規範獲取鎖順序
*/
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread0 = new Thread(new TransferMoneyNotDeadLock(0));
Thread thread1 = new Thread(new TransferMoneyNotDeadLock(1));
thread0.start();
thread1.start();
thread0.join();
thread1.join();
System.out.println("賬戶1餘額:" + account1.blance);
System.out.println("賬戶2餘額:" + account2.blance);
}
@Override
public void run() {
if (flag == 0) {
transMoney(account1, account2, 200);
} else {
transMoney(account2, account1, 200);
}
}
private void transMoney(Account from, Account to, int account) {
// 獲取兩個賬戶的hashcode,通過比較大小規範獲取鎖的順序
int fromHashCode = System.identityHashCode(from);
int toHashCode = System.identityHashCode(to);
if (fromHashCode < toHashCode) {
synchronized (from) {
synchronized (to) {
transfer(from, to, account);
}
}
} else if (fromHashCode > toHashCode) {
synchronized (to) {
synchronized (from) {
transfer(from, to, account);
}
}
} else { // 此時hashcode值相同
synchronized (lock) {
synchronized (from) {
synchronized (to) {
transfer(from, to, account);
}
}
}
}
}
private void transfer(Account from, Account to, int account) {
if (from.blance < account) {
throw new RuntimeException("賬戶餘額不足");
}
from.blance -= account;
to.blance += account;
System.out.println("成功轉賬" + account + "元");
}
static class Account {
int blance;
public Account(int blance) {
this.blance = blance;
}
}
}
案例中,使用鎖的hashcode值決定鎖的順序,在hash衝突時採用加時賽(使用額外的鎖)。如果對象數據有能夠標識順序的字段(例如自增主鍵),則不需要額外的鎖。
實際場景不會直接加鎖,可能用到事務保證轉賬的一致性和原子性,使用消息隊列異步處理,即使加鎖也會用分佈式加鎖。
預防策略–哲學家就餐的換手方案
哲學家就餐問題:假設五位哲學家圍成一桌,兩人中間放置一根筷子。哲學家除了思考就是喫飯,哲學家喫飯時先拿左手邊筷子,再拿右手邊筷子,此時下一位哲學家喫飯只能等待左手邊筷子。如果五位哲學家同時喫飯,同時拿起了左手邊筷子,就會導致死鎖和資源耗盡。
哲學家就餐問題的解決方案:
- 就餐前檢查資源是否足夠–預防免策略
- 改變一個哲學家就餐的順序–預防策略
- 限制同時就餐人數,同一時間最多隻能有4個科學家就餐–預防策略
- 檢查與恢復
避免策略 – 銀行家算法
描述:以銀行借貸分配策略爲基礎,判斷並保證系統處於安全狀態
- 客戶在第一次申請貸款時,聲明所需最大資金量,在滿足所有貸款要求並完成項目時,及時歸還。
- 在客戶貸款數量不大於銀行最大值時,銀行家儘量滿足客戶需要
角色
- 銀行家–操作系統
- 資金–資源
- 客戶–申請資源的線程
思想:銀行的剩餘資源滿足某個申請的未來需要,並回收資源。以此迭代,不斷滿足未來需要、回收資源,構成一個安全序列。不滿足未來需要,表示不安全。
檢查與恢復策略–代碼演示
思想:一段時間檢查是否有死鎖,如果有,剝奪資源,打破死鎖。
【代碼演示】使用顯式鎖Lock中的定時tryLock功能代替內置鎖機制。
public class TryLockDeadLock implements Runnable {
private int flag;
public TryLockDeadLock(int flag) {
this.flag = flag;
}
private static final Lock lock1 = new ReentrantLock();
private static final Lock lock2 = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new TryLockDeadLock(0));
Thread thread2 = new Thread(new TryLockDeadLock(1));
thread1.start();
thread2.start();
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
if (flag == 0) {
try {
if (lock1.tryLock(1, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + "獲取lock1");
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
if (lock2.tryLock(1, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + "獲取lock2,此時擁有兩把鎖");
lock2.unlock();
lock1.unlock();
break;
} else {
System.out.println(Thread.currentThread().getName() + "獲取lock2失敗,重試");
lock1.unlock();
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
}
} else {
System.out.println(Thread.currentThread().getName() + "獲取lock1失敗,重試");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
try {
if (lock2.tryLock(1, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + "獲取lock2");
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
if (lock1.tryLock(1, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + "獲取lock1,此時擁有兩把鎖");
lock1.unlock();
lock2.unlock();
break;
} else {
System.out.println(Thread.currentThread().getName() + "獲取lock1失敗,重試");
lock2.unlock();
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
}
} else {
System.out.println(Thread.currentThread().getName() + "獲取lock2失敗,重試");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
檢測與恢復策略–操作系統概念
檢測算法:鎖的調用鏈路
- 允許系統進入死鎖狀態
- 每次調用鎖都記錄,維護鎖調用鏈路
- 定期檢查鎖的調用鏈路圖,是否存在死鎖
- 一旦發生死鎖,採用死鎖恢復機制進行恢復。
恢復方法
進程終止
- 終止所有線程,重新計算
- 逐個終止線程直到死鎖消失。每終止一個線程都要做死鎖檢測,開銷很大。
資源搶佔
- 選取一個犧牲品(線程),最有效的是講線程回滾到足夠打破死鎖。但某個線程一直被犧牲,可能造成飢餓,因此需要限制回滾次數。
實際開發中如何避免死鎖
1. 設置超時時間
造成超時的可能性大概有:發生死鎖、線程陷入死循環、線程執行慢。
- Lock的tryLock具備超時功能,如果獲取鎖失敗,可以打印日誌以及報警等。
- synchronized不具備嘗試鎖能力。不能中斷試圖獲取monitor鎖的線程
2. 多使用併發類而不是自己設計鎖。
使用併發類發生死鎖的概率低。
- 併發類:ConcurrentHashMap、ConcurrentLinkeedQueue、AatomicBoolean
- 實際應用中java.uti.concurrent.atomic包中的類
- 多使用併發集合少用同步集合 ,併發集合比同步集合的可擴展性更好
- 併發場景需要用到map,首先想到用ConcurrentHashMap
3. 降低鎖的使用粒度:用不同的鎖而不是一個鎖。鎖的保護範圍(臨界區)越小越好。
4. 如果可以使用同步代碼塊,不使用同步方法:自己指定鎖對象。同步方法比同步代碼塊範圍大。
5. 給線程起有意義的名字,便於debug和排查。
6. 避免鎖的嵌套:例如MustDeadLock類
7. 分配資源前先看看資源能不能收回來:銀行家算法
8. 儘量不要多個功能使用同一把鎖:專鎖專用。
活鎖
活鎖:線程沒有阻塞,始終在運行,但是程序得不到進展,重複做同樣的事情。
活鎖與死鎖效果一樣,程序無法正常運行。發生死鎖,線程陷入阻塞;活鎖不會阻塞,會更加耗費CPU資源。
舉例1:類似於死鎖例子,有兩個人見面分別向對方鞠躬,然而處於紳士風度,兩人都不想早於對方起身。因此兩個人分別向對方說“你先請”,陷入循環,最終二人仍未起身。
舉例2:例如哲學家就餐問題,五位哲學家同時拿起左手邊筷子會造成死鎖。如果給鎖加5秒超時,5秒超時後獲取鎖失敗,哲學家再次同時拿起筷子,循環往復,便是活鎖。
活鎖演示–謙讓的紳士
public class LiveLock {
public static void main(String[] args) {
People gentleman1 = new People("紳士1");
People gentleman2 = new People("紳士2");
Action getUpAction = new Action(gentleman1);
new Thread(() -> {
try {
gentleman1.doAction(getUpAction, gentleman2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
gentleman2.doAction(getUpAction, gentleman1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
class Action {
private People executor;
public Action(People executor) {
this.executor = executor;
}
public synchronized void execute() {
System.out.println(executor.getName() + " 執行起身動作");
}
public People getexecutor() {
return executor;
}
public void setexecutor(People executor) {
this.executor = executor;
}
}
class People {
private String name; // 姓名
private boolean isBendOver; // 是否彎腰
public People(String name) {
this.name = name;
this.isBendOver = true;
}
public void doAction(Action action, People other) throws InterruptedException {
while (isBendOver) {
// 如果對方正在起身,等對方先起身
if (action.getexecutor() != this) {
TimeUnit.MILLISECONDS.sleep(1);
continue;
}
// 如果對方仍在彎腰,請對方起身
if (other.isBendOver) {
// 使用隨機元素打破活鎖
// Random rand = new Random();
// if (other.isBendOver && rand.nextInt(10) < 9) {
System.out.println(name + "對" + other.getName() + "說,您先起身。");
action.setexecutor(other);
continue;
}
action.execute();
isBendOver = false;
System.out.println(name + ":我起身了");
action.setexecutor(other);
}
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
活鎖解決方法:加入隨機因素,減小獲取鎖碰撞的概率。
在消息隊列中,如果依賴服務出現問題,例如宕機,消息處理失敗,由於消息在消息隊列頭部,會導致消息隊列一直重試發送消息。
解決方法:(1)放到消息隊列末尾;(2)限制重試上限,超出上限的數據可以持久化到數據庫中,觸發報警機制,等待調度再次調起。
飢餓
出現線程飢餓可能的原因:
- 當線程需要某些資源(一般是CPU),但是始終無法得到。
- 線程優先級設置過低,或者其他線程持有鎖不釋放。
線程飢餓時,無法得到很好地執行,導致系統響應性差。
結合系統優先級描述。