文章首發於公衆號,歡迎訂閱
什麼是死鎖
每個線程都在等待對方線程釋放鎖,然而誰都不主動釋放鎖,結果就構成死鎖。
死鎖的影響在不同系統中是不一樣的,這取決於系統對死鎖的處理能力。
數據庫:檢測並放棄事務。
JVM :無法自動處理。
發生死鎖的例子
經典死鎖
public class DeadLock implements Runnable {
int flag = 1;
static Object lock1 = new Object();
static Object lock2 = new Object();
public static void main(String[] args) {
DeadLock deadLock1 = new DeadLock();
DeadLock deadLock2 = new DeadLock();
deadLock1.flag = 1;
deadLock2.flag = 0;
Thread t1 = new Thread(deadLock1);
Thread t2 = new Thread(deadLock2);
t1.start();
t2.start();
}
@Override
public void run() {
System.out.println("flag = " + flag);
if (flag == 1) {
synchronized (lock1) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("線程1成功拿到兩把鎖");
}
}
}
if (flag == 0) {
synchronized (lock2) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("線程2成功拿到兩把鎖");
}
}
}
}
}
由於 lock1 在等 lock2 釋放鎖,而 lock2 在等 lock1 釋放鎖,從而構成了死鎖。
轉賬
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) {
try {
// 模擬耗時操作
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (to) {
if (from.balance - amount < 0) {
System.out.println("餘額不足,轉賬失敗。");
return;
}
from.balance -= amount;
to.balance = to.balance + amount;
System.out.println("成功轉賬" + amount + "元");
}
}
}
static class Account {
public Account(int balance) {
this.balance = balance;
}
int balance;
}
}
多人隨機轉賬
public class MultiTransferMoney {
// 人越少,死鎖機率越大
private static final int NUM_ACCOUNTS = 50;
private static final int NUM_MONEY = 1000;
private static final int NUM_ITERATIONS = 1000000;
private static final int NUM_THREADS = 20;
public static void main(String[] args) {
Random rnd = new Random();
TransferMoney.Account[] accounts = new TransferMoney.Account[NUM_ACCOUNTS];
for (int i = 0; i < accounts.length; i++) {
accounts[i] = new TransferMoney.Account(NUM_MONEY);
}
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(NUM_MONEY);
TransferMoney.transferMoney(accounts[fromAcct], accounts[toAcct], amount);
}
System.out.println("運行結束");
}
}
for (int i = 0; i < NUM_THREADS; i++) {
new TransferThread().start();
}
}
}
死鎖的必要條件
死鎖的產生具備以下四個條件:
- 互斥條件:指線程對己經獲取到的資源進行排它性使用, 即該資源同時只由一個線程佔用。如果此時還有其他線程請求獲取該資源,則請求者只能等待,直至佔有資源的線程釋放該資源。
- 請求並持有條件: 指一個線程己經持有了至少一個資源 , 但又提出了新的資源請求 ,而新資源己被其他線程佔有,所以當前線程會被阻塞,但阻塞的同時並不釋放自己己經獲取的資源。
- 不可剝奪條件: 指線程獲取到的資源在自己使用完之前不能被其他線程搶佔,只有在自己使用完畢後才由自己釋放該資源。
- 環路等待條件:指在發生死鎖時,必然存在一個線程→資源的環形鏈,即線程集合{ T0,T1,T2 ,…,Tn }中的 T0 正在等待一個 T1 佔用的資源,T1 正在等待 T2 佔用的資源,……Tn 正在等待己被 T0 佔用的資源。
來看上面的經典死鎖示例是如何滿足這四個條件的。
首先,lock1 和 lock2 都是互斥資源,當線程 1 調用
synchronized(lock1)
方法獲取到 lock1 鎖並釋放前, 線程 2 再調用synchronized(lock1)
方法嘗試獲取該資源會被阻塞,只有線程 1 主動釋放該鎖,線程 2 才能獲得,這滿足了資源互斥條件 。 線程 1 首先通過synchronized(lock1)
方法獲取到 lock1 鎖,然後通過synchronized(lock2)
方法等待獲取 lock2 鎖,這就構成了請求並持有條件。 線程 1 在獲取 lock1 鎖後,該資源不會被線程 2 掠奪走 , 只有線程 1 自己主動釋放 lock1 資源時,它纔會放棄對該資源的持有權 ,這構成了資源的不可剝奪條件 。 線程 1 持有 lock1 並等待獲取 lock2 ,而線程 2 持有 lcok2 資源並等待 lock1 資源,這構成了環路等待條件 。
如何定位死鎖
- 利用 jstack 查看線程 pid
- 利用 JMX 的
ThreadMXBean
public class ThreadMXBeanDetection implements Runnable {
int flag = 1;
static Object lock1 = new Object();
static Object lock2 = new Object();
public static void main(String[] args) throws InterruptedException {
ThreadMXBeanDetection r1 = new ThreadMXBeanDetection();
ThreadMXBeanDetection r2 = new ThreadMXBeanDetection();
r1.flag = 1;
r2.flag = 0;
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
Thread.sleep(1000);
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads != null && deadlockedThreads.length > 0) {
for (long deadlockedThread : deadlockedThreads) {
ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThread);
System.out.println("發現死鎖" + threadInfo.getThreadName());
}
}
}
@Override
public void run() {
System.out.println("flag = " + flag);
if (flag == 1) {
synchronized (lock1) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("線程1成功拿到兩把鎖");
}
}
}
if (flag == 0) {
synchronized (lock2) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("線程2成功拿到兩把鎖");
}
}
}
}
}
修復死鎖的策略
避免策略
改變獲取鎖的順序,避免相反的獲取鎖的順序。
修改之前轉賬示例的代碼:
public class TransferMoney implements Runnable {
int flag = 1;
static Account a = new Account(500);
static Account b = new Account(500);
static Object lock = new Object();
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) {
class Helper {
public void transfer() {
if (from.balance - amount < 0) {
System.out.println("餘額不足,轉賬失敗。");
return;
}
from.balance -= amount;
to.balance = to.balance + amount;
System.out.println("成功轉賬" + amount + "元");
}
}
// 通過hashCode來決定獲取鎖的順序
int fromHash = System.identityHashCode(from);
int toHash = System.identityHashCode(to);
if (fromHash < toHash) {
synchronized (from) {
synchronized (to) {
new Helper().transfer();
}
}
} else if (fromHash > toHash) {
synchronized (to) {
synchronized (from) {
new Helper().transfer();
}
}
} else {
// 如果發生哈希衝突,則需要進行“加時賽”
synchronized (lock) {
synchronized (to) {
synchronized (from) {
new Helper().transfer();
}
}
}
}
}
static class Account {
public Account(int balance) {
this.balance = balance;
}
int balance;
}
}
爲了避免哈希衝突,可以使用主鍵,一般主鍵是唯一的。
檢測與修復策略
一段時間檢測是否有死鎖,如果有就剝奪某一個資源,來打開死鎖。
恢復方法1:進程終止
逐個終止線程,直到死鎖消除。
終止順序:
- 優先級(是前臺交互還是後臺處理)
- 已佔用資源、還需要的資源
- 已經運行時間
恢復方法2:資源搶佔
把已經分發出去的鎖給收回來。讓線程回退幾步,這樣就不用結束整個線程,成本比較低。
缺點:可能同一個線程一直被搶佔,那就造成飢餓
鴕鳥策略
鴕鳥這種動物在遇到危險的時候,通常就會把頭埋在地上,這樣一來它就看不到危險了。而蛇鳥策略的意思就是說,如果我們發生死鎖的概率極其低,那麼我們就直接忽略它,直到死鎖發生的時候,再人工修復。
如何避免死鎖
1、設置超時時間,利用Lock
的 tryLock(long timeout, TimeUnit unit)
方法。
public class TryLockDeadlock implements Runnable {
int flag = 1;
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
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 {
if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
System.out.println("線程1獲取到了鎖1");
Thread.sleep(new Random().nextInt(1000));
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 {
if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {
System.out.println("線程2獲取到了鎖2");
Thread.sleep(new Random().nextInt(1000));
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嘗試獲取鎖1失敗,已重試");
lock2.unlock();
Thread.sleep(new Random().nextInt(1000));
}
} else {
System.out.println("線程2獲取鎖2失敗,已重試");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
2、多使用併發類而不是自己設計鎖。
3、儘量降低鎖的使用粒度:用不同的鎖而不是一個鎖。
4、如果能使用同步代碼塊,就不使用同步方法。
5、給你的線程起個有意義的名字:debug 和排查時事半功倍,框架和 JDK 都遵守這個最佳實踐。
6、避免鎖的嵌套。
7、分配資源前先看能不能收回來:銀行家算法。
8、儘量不要幾個功能用同一把鎖:專鎖專用。
活鎖
線程主動將資源釋放給他人使用,那麼就會導致資源不斷地在兩個線程間跳動,而沒有一個線程可以同時拿到所有資源正常執行,這種情況就是活鎖。
public class LiveLock {
static class Spoon {
private Diner owner;
public Spoon(Diner owner) {
this.owner = owner;
}
public Diner getOwner() {
return owner;
}
public void setOwner(Diner owner) {
this.owner = owner;
}
public synchronized void use() {
System.out.printf("%s吃完了!", owner.name);
}
}
static class Diner {
private String name;
private boolean isHungry;
public Diner(String name) {
this.name = name;
isHungry = true;
}
public void eatWith(Spoon spoon, Diner spouse) {
while (isHungry) {
if (spoon.owner != this) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
continue;
}
if (spouse.isHungry) {
System.out.println(name + ": 親愛的" + spouse.name + "你先吃吧");
spoon.setOwner(spouse);
continue;
}
spoon.use();
isHungry = false;
System.out.println(name + ": 我吃完了");
spoon.setOwner(spouse);
}
}
}
public static void main(String[] args) {
Diner husband = new Diner("牛郎");
Diner wife = new Diner("織女");
Spoon spoon = new Spoon(husband);
new Thread(new Runnable() {
@Override
public void run() {
husband.eatWith(spoon, wife);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
wife.eatWith(spoon, husband);
}
}).start();
}
}
上述代碼會無限執行下去。
解決方案:加入隨機因素,使其退出互相謙讓。還有其他如以太網的指數退避算法。
// 隨機因素
Random random = new Random();
if (spouse.isHungry && random.nextInt(10) < 9) {
System.out.println(name + ": 親愛的" + spouse.name + "你先吃吧");
spoon.setOwner(spouse);
continue;
}
工作中的活鎖:消息隊列
如果消息如果處理失敗,就放在隊列頭重試,那麼可能處理該消息一直失敗。雖然沒阻塞,但程序無法繼續。
解決方案:放在隊尾或者添加重試機制,比如重試超過一定次數,就把該任務放入數據庫,數據庫檢測到新的任務,那麼後續定時任務會嘗試繼續執行該任務。
飢餓
飢餓指某一個或者多個線程因爲種種原因無法獲得所需要的資源,導致一直無法執行。比如它的線程優先級可能太低,而高優先級的線程不斷搶佔它需要的資源,導致低優先級線程無法工作。與死鎖相比,飢餓還是有可能在未來一段時間內解決的(比如,高優先級的線程已經完成任務,不再瘋狂執行)。
我創建了一個免費的知識星球,用於分享知識日記,歡迎加入!