解決多線程業務下 死鎖和活鎖的問題

關於死鎖和活鎖的概念

死鎖:

是指兩個或兩個以上的進程(或線程)在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。

產生死鎖的必要條件:
互斥條件:所謂互斥就是進程在某一時間內獨佔資源。
請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
不剝奪條件:進程已獲得資源,在末使用完之前,不能強行剝奪。
循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關係。

活鎖:

任務或者執行者沒有被阻塞,由於某些條件沒有滿足,導致一直重複嘗試,失敗,嘗試,失敗。

活鎖和死鎖的區別

在於,處於活鎖的實體是在不斷的改變狀態,所謂的“活”, 而處於死鎖的實體表現爲等待;活鎖有可能自行解開,死鎖則不能。

業務場景

我們模擬最常見的轉賬業務: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));
		}
	}
}

執行結果:正常!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章