1.死鎖是什么?有什么危害?
1.1 什么是死鎖
- 發(fā)生在并發(fā)中
- 互不相讓:當(dāng)兩個(gè)(或更多)線程(或進(jìn)程)相互持有對(duì)方所需要的資源,又不主動(dòng)釋放,導(dǎo)致所有人都無法繼續(xù)前進(jìn),導(dǎo)致程序陷入無盡的阻塞,這就是死鎖。
- 多個(gè)線程造成死鎖的情況
1.2 死鎖的影響
死鎖的影響在不同系統(tǒng)中是不一樣的,這取決于系統(tǒng)對(duì)死鎖的處理能力
- 數(shù)據(jù)庫中:檢測(cè)并放棄事務(wù)
- JVM中:無法自動(dòng)處理
幾率不高但危害大
- 不一定發(fā)生,但是遵守"墨菲定律"
- 一旦發(fā)生,多是高并發(fā)場(chǎng)景,影響用戶多
- 整個(gè)系統(tǒng)崩潰、子系統(tǒng)崩潰、性能降低
- 壓力測(cè)試無法找出所有潛在的死鎖
1.3 發(fā)生死鎖的例子
必定發(fā)生死鎖的情況
/**
* 描述: 必定發(fā)生死鎖的情況
*/
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("flag = " + flag);
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("線程1成功拿到兩把鎖");
}
}
}
if (flag == 0) {
synchronized (o2) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("線程2成功拿到兩把鎖");
}
}
}
}
}
flag = 1
flag = 0
多個(gè)人相互轉(zhuǎn)賬
- 需要兩把鎖
- 獲取兩把鎖成功,且余額大于0,則扣除轉(zhuǎn)出人,增加收款人的余額,是原子操作
-
順序相反
導(dǎo)致死鎖
public class MultiTransferMoney {
private static final int NUM_ACCOUNTS = 500;
private static final int NUM_MONEY = 1000;
private static final int NUM_ITERATIONS = 1000000;
private static final int NUM_THREADS = 100;
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("運(yùn)行結(jié)束");
}
}
for (int i = 0; i < NUM_THREADS; i++) {
new TransferThread().start();
}
}
}
public class TransferMoney implements Runnable {
static Account a = new Account(500);
static Account b = new Account(500);
static Object lock = new Object();
int flag = 1;
public static void transferMoney(Account from, Account to, int amount) {
class Helper {
public void transfer() {
if (from.balance - amount < 0) {
System.out.println("余額不足,轉(zhuǎn)賬失敗。");
return;
}
from.balance -= amount;
to.balance = to.balance + amount;
System.out.println("成功轉(zhuǎn)賬" + amount + "元");
}
}
synchronized (from) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (to) {
new Helper().transfer();
}
}
}
@Override
public void run() {
if (flag == 1) {
transferMoney(a, b, 200);
}
if (flag == 0) {
transferMoney(b, a, 200);
}
}
static class Account {
int balance;
public Account(int balance) {
this.balance = balance;
}
}
}
死鎖
程序停止輸出發(fā)生死鎖
2. 死鎖的4個(gè)必要條件:
- 互斥條件
- 請(qǐng)求與保持條件
- 不剝奪條件
- 循環(huán)等待條件
3. 如何定位死鎖
3.1.jstack定位死鎖
3.2.ThreadMXBean定位死鎖
public class ThreadMXBeanDetection implements Runnable {
int flag = 1;
static Object o1 = new Object();
static Object o2 = 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 (int i = 0; i < deadlockedThreads.length; i++) {
ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);
System.out.println("發(fā)現(xiàn)死鎖" + threadInfo.getThreadName());
}
}
}
@Override
public void run() {
System.out.println("flag = " + flag);
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("線程1成功拿到兩把鎖");
}
}
}
if (flag == 0) {
synchronized (o2) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("線程2成功拿到兩把鎖");
}
}
}
}
}
flag = 0
flag = 1
發(fā)現(xiàn)死鎖Thread-1
發(fā)現(xiàn)死鎖Thread-0
4. 修復(fù)死鎖策略
線上發(fā)生死鎖應(yīng)該什么辦
- 線上問題都需要防患與未然,不造成損失幾乎是不可能
- 保存死鎖數(shù)據(jù),然后立刻重啟服務(wù)器
- 暫時(shí)保證線上服務(wù)的安全,然后再利用剛才保存的信息,排查死鎖,修改代碼,重新發(fā)版
常見修復(fù)策略
4.1. 避免策略
示例1 修改兩人轉(zhuǎn)賬時(shí)獲取鎖的順序
經(jīng)過思考,我們可以發(fā)現(xiàn),其實(shí)轉(zhuǎn)賬時(shí),
并不在乎兩把鎖的相對(duì)獲取順序
。轉(zhuǎn)賬的時(shí)候,我們無論先獲取到轉(zhuǎn)出賬戶鎖對(duì)象,還是先獲取到轉(zhuǎn)入賬戶鎖對(duì)象,只要最終能拿到兩把鎖,就能進(jìn)行安全的操作。所以我們來調(diào)整一下獲取鎖的順序,使得先獲取的賬戶和該賬戶是“轉(zhuǎn)入”或“轉(zhuǎn)出”無關(guān),而是使用 HashCode 的值來決定順序,從而保證線程安全
public static void transferMoney(Account from, Account to, int amount) {
class Helper {
public void transfer() {
if (from.balance - amount < 0) {
System.out.println("余額不足,轉(zhuǎn)賬失敗。");
return;
}
from.balance -= amount;
to.balance = to.balance + amount;
System.out.println("成功轉(zhuǎn)賬" + amount + "元");
}
}
int fromHash = System.identityHashCode(from);
int toHash = System.identityHashCode(to);
if (fromHash < toHash) {
synchronized (from) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (to) {
new Helper().transfer();
}
}
}
else if (fromHash > toHash) {
synchronized (to) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (from) {
new Helper().transfer();
}
}
}else {
synchronized (lock) {
synchronized (to) {
synchronized (from) {
new Helper().transfer();
}
}
}
}
}
可以看到,我們會(huì)分別計(jì)算出這兩個(gè) Account 的 HashCode,然后根據(jù) HashCode 的大小來決定獲取鎖的順序。這樣一來,不論是哪個(gè)線程先執(zhí)行,不論是轉(zhuǎn)出還是被轉(zhuǎn)入,它獲取鎖的順序都會(huì)嚴(yán)格根據(jù) HashCode 的值來決定,那么大家獲取鎖的順序就一樣了,就不會(huì)出現(xiàn)獲取鎖順序相反的情況,也就避免了死鎖
總結(jié):通過hashcode來決定獲取鎖的順序、沖突時(shí)需要“加時(shí)賽”(再加一把鎖)獲取鎖
示例2 哲學(xué)家換手解決.改變一個(gè)哲學(xué)家拿筷子的順序.
/**
* 描述: 演示哲學(xué)家就餐問題導(dǎo)致的死鎖
*/
public class DiningForPhilosophers {
/**
* 哲學(xué)家類
*/
public static class Philosophers implements Runnable{
private Object leftChopsticks;
private Object rightChopsticks;
public Philosophers(Object leftChopsticks, Object rightChopsticks) {
this.leftChopsticks = leftChopsticks;
this.rightChopsticks = rightChopsticks;
}
@Override
public void run() {
try {
while(true){
action("思考......");
synchronized (leftChopsticks){
action("拿起左邊的筷子");
synchronized (rightChopsticks){
action("拿起右邊的筷子---吃飯");
action("放下右邊的筷子");
}
action("放下左邊的筷子");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void action(String action) throws InterruptedException {
System.out.println(Thread.currentThread().getName()+"-->"+action);
//隨機(jī)休眠,代表這個(gè)行為執(zhí)行的耗時(shí)
Thread.sleep((long) (Math.random()*10));
}
};
public static void main(String[] args) {
Philosophers[] philosophers = new Philosophers[5];
Object[] chopsticks = new Object[philosophers.length];
//初始化筷子對(duì)象
for (int i=0;i<chopsticks.length;i++){
chopsticks[i] = new Object();
}
//初始化哲學(xué)家對(duì)象,并啟動(dòng)線程.
for (int i=0;i<chopsticks.length;i++){
Object leftChopsticks = chopsticks[i];
Object rightChopsticks = chopsticks[(i+1)%chopsticks.length];
philosophers[i] = new Philosophers(leftChopsticks, rightChopsticks);
new Thread(philosophers[i],"哲學(xué)家"+i+"號(hào)").start();
}
}
}
同樣發(fā)生死鎖,這里大家陷入了一種循環(huán)等待的狀態(tài),0號(hào)獲取了他左邊的0號(hào)筷子,請(qǐng)求他右邊的1號(hào)筷子,1號(hào)獲取了左邊的1號(hào)筷子,請(qǐng)求等待他右邊的2號(hào)筷子...........5號(hào)獲取了他左邊的5號(hào)筷子等待他右手邊的0號(hào)筷子........這樣就形成了一個(gè)環(huán).
哲學(xué)家問題解決策略
- 服務(wù)員檢查
哲學(xué)家拿叉子時(shí) 服務(wù)員檢查叉子數(shù)量 - 改變一個(gè)哲學(xué)家拿叉子的順序
- 餐票
哲學(xué)家就餐時(shí)先拿到餐票就能就餐 - 領(lǐng)導(dǎo)調(diào)節(jié)(檢測(cè)與恢復(fù)策略)
這里演示改變一個(gè)哲學(xué)家拿叉子的順序修復(fù)
這樣就成功的避免了死鎖
4.2 檢測(cè)與恢復(fù)策略:一段時(shí)間檢測(cè)是否有死鎖,如果有就剝奪某個(gè)資源,來解除死鎖
允許死鎖的發(fā)生,但是發(fā)生死鎖后要記錄下來并通過停止線程或其他方式停止死鎖
死鎖檢測(cè)算法
- 允許發(fā)生死鎖
- 每次調(diào)用鎖的
記錄
- 定期檢查"
鎖的調(diào)用鏈路圖
"中是否存在環(huán)路 - 一旦發(fā)生死鎖,就用死鎖恢復(fù)機(jī)制進(jìn)行
恢復(fù)
機(jī)制進(jìn)行恢復(fù)
恢復(fù)方法1:進(jìn)程終止
- 逐個(gè)終止線程,直到死鎖消除。
- 終止順序:
- 優(yōu)先級(jí)(重要性,是前臺(tái)交互還是后臺(tái)處理)
- 已占用資源、還需要的資源
- 已經(jīng)運(yùn)行時(shí)間
恢復(fù)方法2:資源搶占
- 把已經(jīng)分發(fā)出去的鎖給
收回來
- 讓線程
回退幾步
,這樣就不用結(jié)束整個(gè)線程,成本比較低
比如 讓哲學(xué)家把拿起的筷子再放下 - 缺點(diǎn):可能同一個(gè)線程一直被搶占,那就造成
饑餓
4.3 鴕鳥策略
死鎖發(fā)生的幾率特別小,忽略他,等死鎖發(fā)生了,再去處理修改
5 實(shí)際工程中如何避免死鎖
1. 設(shè)置超時(shí)
時(shí)間
- Lock的tryLock(long timeout,TimeUnit unit)
造成超時(shí)的可能性很多,發(fā)生了死鎖,線程陷入了死循環(huán),線程執(zhí)行很慢.
/**
* 描述: 用tryLock來避免死鎖
*/
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();
}
}
}
}
}
線程1獲取到了鎖1
線程2獲取到了鎖2
線程1嘗試獲取鎖2失敗,已重試
線程2獲取到了鎖1
線程2成功獲取到了兩把鎖
線程1獲取到了鎖1
線程1獲取到了鎖2
線程1成功獲取到了兩把鎖
2.多使用并發(fā)類而不是自己設(shè)計(jì)鎖
如ConcurrentHashMap
java.util.concurrent.atomic.
3.盡量降低鎖的使用粒度:用不同的鎖而不是一個(gè)鎖
縮小鎖的臨界區(qū)
4.如果能使用同步代碼塊,就不使用同步方法:自己指定鎖對(duì)象
縮小了同步范圍,可以自己指定鎖對(duì)象