關於死鎖和活鎖的概念
死鎖:
是指兩個或兩個以上的進程(或線程)在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。
產生死鎖的必要條件:
互斥條件:所謂互斥就是進程在某一時間內獨佔資源。
請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
不剝奪條件:進程已獲得資源,在末使用完之前,不能強行剝奪。
循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關係。
活鎖:
任務或者執行者沒有被阻塞,由於某些條件沒有滿足,導致一直重複嘗試,失敗,嘗試,失敗。
活鎖和死鎖的區別
在於,處於活鎖的實體是在不斷的改變狀態,所謂的“活”, 而處於死鎖的實體表現爲等待;活鎖有可能自行解開,死鎖則不能。
業務場景
我們模擬最常見的轉賬業務:A賬戶給B賬戶轉賬的同時, B賬戶又在給A賬戶轉賬。此時會發生死鎖。
模擬死鎖開始準備
新建一個賬戶類
/**
* 用戶賬戶類
*/
public class UserAccount {
//賬戶id(唯一的值)可能是銀行卡號
private final String id;
//賬戶餘額
private Double money;
public UserAccount(String id, Double amount) {
this.id = id;
this.money = amount;
}
public String getId() {
return id;
}
public Double getMoney() {
return money;
}
@Override
public String toString() {
return "UserAccount [id=" + id + ", money=" + money + "]";
}
/**
* 轉入資金
* @param amout 金額
*/
public void addMoney(double amout) {
money = money + amout;
System.out.println("賬戶:" + id + " ,轉入"+ amout + "元,當前賬戶餘額:" + money);
}
/** 轉出資金
* @param amout 金額
*/
public void flyMoney(double amout) {
if ((money - amout) >0) {
money = money - amout;
System.out.println("賬戶:" + id + " ,轉出"+ amout + "元,當前賬戶餘額:" + money);
}else {
System.out.println("賬戶餘額不足!當前餘額:" + money);
}
}
}
定義一個交易的接口
/**
* 轉賬動作接口
*/
public interface ITransfer {
/**
*
* @param from 轉出賬戶
* @param to 轉入賬戶
* @param amount 轉賬金額
* @throws InterruptedException
*/
void transfer(UserAccount from, UserAccount to, double amount) throws InterruptedException;
}
創建一個執行交易任務的線程類
// 執行交易的線程任務
private static class TransferThread extends Thread{
// 線程名稱
private String name;
// 轉出賬戶
private UserAccount from;
// 轉入賬戶
private UserAccount to;
// 交易金額
private Double amount;
// 交易方式
private ITransfer transfer;
public TransferThread(String name, UserAccount from, UserAccount to, Double amount, ITransfer transfer) {
super();
this.name = name;
this.from = from;
this.to = to;
this.amount = amount;
this.transfer = transfer;
}
@Override
public void run() {
Thread.currentThread().setName(name);
try {
transfer.transfer(from,to,amount);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
模擬一個不安全的交易
/**
* 不安全的交易方式實現
* @author James Lee
*
*/
public class UnSafeTransfer implements ITransfer{
@Override
public void transfer(UserAccount from, UserAccount to, double amount) throws InterruptedException {
// 鎖定轉出賬戶
synchronized (from) {
System.out.println(Thread.currentThread().getName()+" 拿到【 " + from.getId() + "】賬戶的執行權!" );
SleepTools.ms(1000);
// 再鎖定轉入賬戶
synchronized (to) {
System.out.println(Thread.currentThread().getName()+" 拿到【 " + to.getId() + "】賬戶的執行權!" );
// 這裏代表兩個賬戶的鎖都拿到了纔可以實現交易
from.flyMoney(amount);
to.addMoney(amount);
}
}
}
}
public static void main(String[] args) {
UserAccount james = new UserAccount("622...181: james", 100.00);
UserAccount kobe = new UserAccount("622...182: kobe", 100.00);
ITransfer transfer = new UnSafeTransfer();
// 模擬 james給kobe轉賬20塊錢
TransferThread transferThread = new TransferThread("交易線程一", james, kobe, 20.00, transfer);
// 模擬 kobe給james轉賬50塊錢
TransferThread transferThread2 = new TransferThread("交易線程二", kobe, james, 50.00, transfer);
transferThread.start();
transferThread2.start();
}
測試結果:
發生的原因:
兩個交易線程一直在等對方釋放鎖。
解決思路
解決死鎖的思路:就是保證線程的順序性。
有了思路以後,我們可以再在程序裏控制鎖賬戶的順序性。
死鎖解決辦法一
根據實體的hashCode來判定,hashCode低的先鎖定,高的後鎖定,使用synchronized 加鎖
代碼實現:
import xiangxue.day09.bank.UserAccount;
import xiangxue.tools.SleepTools;
/**
* 不會產生死鎖的安全轉賬: 基於hashCode或者實體的唯一主鍵
*
*/
public class SafeTransferByHashCode implements ITransfer {
private static Object tieLock = new Object();//加時賽鎖
@Override
public void transfer(UserAccount from, UserAccount to, double amount) throws InterruptedException {
// 首先獲取實體賬戶的hashcode,考慮有可能傳入之前會重寫hashcode,所以我們調用jdk的identityHashCode獲取原生值
int fromHash = System.identityHashCode(from);
int toHash = System.identityHashCode(to);
// 先鎖hash值小的賬戶
if (fromHash < toHash) {
synchronized (from) {
System.out.println(Thread.currentThread().getName() + " 拿到【 " + from.getId() + "】賬戶的執行權!");
SleepTools.ms(1000);
// 再鎖定轉入賬戶
synchronized (to) {
System.out.println(Thread.currentThread().getName() + " 拿到【 " + to.getId() + "】賬戶的執行權!");
// 這裏代表兩個賬戶的鎖都拿到了纔可以實現交易
from.flyMoney(amount);
to.addMoney(amount);
}
}
}
else if (toHash < fromHash) {
synchronized (to) {
System.out.println(Thread.currentThread().getName() + " 拿到【 " + to.getId() + "】賬戶的執行權!");
SleepTools.ms(1000);
// 再鎖定轉入賬戶
synchronized (from) {
System.out.println(Thread.currentThread().getName() + " 拿到【 " + from.getId() + "】賬戶的執行權!");
// 這裏代表兩個賬戶的鎖都拿到了纔可以實現交易
from.flyMoney(amount);
to.addMoney(amount);
}
}
}
// 考慮到有hash衝突,這裏在構造一把鎖,讓線程搶佔,誰先搶佔到,就執行。類似於籃球比賽,打成平手後的加時賽
// hash衝突的概率是千萬分之一,鎖三次,對整體的性能其實影響不大
else {
synchronized (tieLock) {
// 這裏先鎖哪個賬戶的順序已經不重要了
synchronized (from) {
System.out.println(Thread.currentThread().getName() + " 拿到【 " + from.getId() + "】賬戶的執行權!");
synchronized (to) {
System.out.println(Thread.currentThread().getName() + " 拿到【 " + to.getId() + "】賬戶的執行權!");
// 這裏代表兩個賬戶的鎖都拿到了纔可以實現交易
from.flyMoney(amount);
to.addMoney(amount);
}
}
}
}
}
}
public static void main(String[] args) {
PayCompany payCompany = new PayCompany();
UserAccount james = new UserAccount("622...181: james", 100.00);
UserAccount kobe = new UserAccount("622...182: kobe", 100.00);
ITransfer transfer = new SafeTransferByHashCode();
// 模擬 james給kobe轉賬20塊錢
TransferThread transferThread = new TransferThread("交易線程一", james, kobe, 20.00, transfer);
// 模擬 kobe給james轉賬50塊錢
TransferThread transferThread2 = new TransferThread("交易線程二", kobe, james, 50.00, transfer);
transferThread.start();
transferThread2.start();
}
測試結果:正常交易了!
代碼分析
簡潔明瞭,容易理解,不足是代碼太多了,可能初中級java開發會這樣。sync是獨佔鎖,瞭解JUC編程的可能還知道一種可重入鎖,Lock。
死鎖解決辦法二
使用lock 嘗試性的拿鎖
我們在用戶賬戶類UserAccount.java 上加上以下代碼:
//顯示鎖
private final Lock lock = new ReentrantLock();
public Lock getLock() {
return lock;
}
import xiangxue.day09.bank.UserAccount;
/**
* 不會產生死鎖的安全轉賬: 使用可重入鎖
*/
public class SafeTransferByLock implements ITransfer{
@Override
public void transfer(UserAccount from, UserAccount to, double amount) throws InterruptedException {
while (true) {
// 嘗試拿到轉出賬戶的鎖
if (from.getLock().tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + " 拿到【 " + from.getId() + "】賬戶的執行權!");
// 再嘗試拿轉入賬戶的鎖
if (to.getLock().tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + " 拿到【 " + to.getId() + "】賬戶的執行權!");
// 這裏代表兩個賬戶的鎖都拿到了纔可以實現交易
from.flyMoney(amount);
to.addMoney(amount);
break;
} finally {
// 釋放轉入賬戶的鎖
to.getLock().unlock();
}
}
} finally {
// 釋放轉出賬戶的鎖
from.getLock().unlock();
}
}
}
}
}
public static void main(String[] args) {
PayCompany payCompany = new PayCompany();
UserAccount james = new UserAccount("622...181: james", 100.00);
UserAccount kobe = new UserAccount("622...182: kobe", 100.00);
ITransfer transfer = new SafeTransferByLock();
// 模擬 james給kobe轉賬20塊錢
TransferThread transferThread = new TransferThread("交易線程一", james, kobe, 20.00, transfer);
// 模擬 kobe給james轉賬50塊錢
TransferThread transferThread2 = new TransferThread("交易線程二", kobe, james, 50.00, transfer);
transferThread.start();
transferThread2.start();
}
測試結果:發生了活鎖
解決活鎖的辦法
在獲取線程鎖加上時間間隔,讓程序休眠
改進SafeTransferByLock.java
import java.util.Random;
import xiangxue.day09.bank.UserAccount;
import xiangxue.tools.SleepTools;
/**
* 不會產生死鎖的安全轉賬: 使用可重入鎖
*/
public class SafeTransferByLock implements ITransfer{
@Override
public void transfer(UserAccount from, UserAccount to, double amount) throws InterruptedException {
Random r = new Random();
while (true) {
// 嘗試拿到轉出賬戶的鎖
if (from.getLock().tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + " 拿到【 " + from.getId() + "】賬戶的執行權!");
// 再嘗試拿轉入賬戶的鎖
if (to.getLock().tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + " 拿到【 " + to.getId() + "】賬戶的執行權!");
// 這裏代表兩個賬戶的鎖都拿到了纔可以實現交易
from.flyMoney(amount);
to.addMoney(amount);
break;
} finally {
// 釋放轉入賬戶的鎖
to.getLock().unlock();
}
}
} finally {
// 釋放轉出賬戶的鎖
from.getLock().unlock();
}
}
// 休眠。解決活鎖
SleepTools.ms(r.nextInt(10));
}
}
}