1. 程式人生 > >多執行緒(二)執行緒互動之互斥與同步

多執行緒(二)執行緒互動之互斥與同步

首先我們通過一個有意思的案例來引入由於執行緒爭用條件造成的一些嚴重的問題。

下面的程式碼簡單來說是初始化多個能量盒子,每個盒子所含初始能量相同,這樣總能量就固定了。開設多個執行緒將這些盒子的能量相互轉移,在轉移過程就出現了問題。

package disappearEnergy;

/**
 * 宇宙的能量系統
 * 遵循能量守恆定律:
 * 能量不會憑空創生或消失,只會從一處轉移到另一處
 */
public class EnergySystem {
	
	//能量盒子,能量存貯的地方
	 private final double[] energyBoxes;

	 
	 /**
	  * 
	  * @param n    能量盒子的數量
	  * @param initialEnergy 每個能量盒子初始含有的能量值
	  */
	 public EnergySystem(int n, double initialEnergy){
		 energyBoxes = new double[n];
		 for (int i = 0; i < energyBoxes.length; i++)
			 energyBoxes[i] = initialEnergy;
	 }
	 
	 /**
	  * 能量的轉移,從一個盒子到另一個盒子
	  * @param from 能量源
	  * @param to     能量終點 
	  * @param amount 能量值
	  */
	 public void transfer(int from, int to, double amount){
		 
			 if (energyBoxes[from] < amount)
				 return;
			 System.out.print(Thread.currentThread().getName());
			 energyBoxes[from] -= amount;
			 System.out.printf("從%d轉移%10.2f單位能量到%d", from, amount, to);
			 energyBoxes[to] += amount;
			 System.out.printf(" 能量總和:%10.2f%n", getTotalEnergies());

	 }
	 
	 /**
	  * 獲取能量世界的能量總和 
	  */
	 public double getTotalEnergies(){
		 double sum = 0;
		 for (double amount : energyBoxes)
			 sum += amount;
		 return sum;
	 }
	 
	 /**
	  * 返回能量盒子的長度
	  */
	 public  int getBoxAmount(){
		 return energyBoxes.length;
	 }
	 
}
package disappearEnergy;

public class EnergyTransferTask implements Runnable{

	//共享的能量世界
	private EnergySystem energySystem;
	//能量轉移的源能量盒子下標
	private int fromBox;
	//單次能量轉移最大單元
	private double maxAmount;
	//最大休眠時間(毫秒)
	private int DELAY = 10;
	
	public EnergyTransferTask(EnergySystem energySystem, int from, double max){
		this.energySystem = energySystem;
		this.fromBox = from;
		this.maxAmount = max;
	}
	
	public void run() {
		try{
			while (true){
				int toBox = (int) (energySystem.getBoxAmount()* Math.random());
				double amount = maxAmount * Math.random();
				energySystem.transfer(fromBox, toBox, amount);
				Thread.sleep((int) (DELAY * Math.random()));
			}
		}catch (InterruptedException e){
			e.printStackTrace();
		}
	}

}
package disappearEnergy;

public class EnergySystemTest {

	//將要構建的能量世界中能量盒子數量
	public static final int BOX_AMOUNT = 100;
	//每個盒子初始能量
    public static final double INITIAL_ENERGY = 1000;

    public static void main(String[] args){
    	EnergySystem eng = new EnergySystem(BOX_AMOUNT, INITIAL_ENERGY);
    	for (int i = 0; i < BOX_AMOUNT; i++){
    		EnergyTransferTask task = new EnergyTransferTask(eng, i, INITIAL_ENERGY);
    		Thread t = new Thread(task,"TransferThread_"+i);
    		t.start();
    	}
    }

}

經過允許,程式出現瞭如下的異常結果:

如上圖所示,能量總和會減少,並且出現某些語句未執行完下一條語句便開始執行的情況。

出現上述問題的原因:

當多個執行緒同時共享訪問同一資料(記憶體區域)時,每個執行緒都嘗試操作該資料,從而導致資料被破壞(corrupted),這種現象稱為爭用條件。

類似於上圖所示的現象,每個執行緒在操作資料時,會先將資料初值讀【取到自己獲得的記憶體中】,然後在記憶體中進行運算後,重新賦值到資料。執行緒1在還【未重新將值賦回去時】,執行緒1阻塞,執行緒2開始訪問該資料,然後進行了修改,之後被阻塞的執行緒1再獲得資源,而將之前計算的值覆蓋掉執行緒2所修改的值,就出現了資料丟失情況。

為解決上面的問題,就要實現執行緒的互斥與同步。

互斥:同一時間,只能有一個執行緒訪問資料。
同步:一種通訊機制,一個執行緒操作完成後,以某種方式通知其他執行緒。

執行緒之間的互斥:通過加鎖實現:執行緒訪問臨界區的程式碼放在一個程式碼塊中,加鎖實現,即 synchronized(lockObj)
執行緒之間的同步:通過wait()+notify()的通訊機制來實現
wait()和notifyAll()是線上程同步的時候使用的一對方法。據此思想,修改了EnergySystem的程式碼,在transfer函式中添加了物件鎖。

package disappearEnergy;

/**
 * 宇宙的能量系統
 * 遵循能量守恆定律:
 * 能量不會憑空創生或消失,只會從一處轉移到另一處
 */
public class EnergySystem {
	
	//能量盒子,能量存貯的地方
	 private final double[] energyBoxes;
	 private final Object lockObj = new Object();
	 
	 /**
	  * 
	  * @param n    能量盒子的數量
	  * @param initialEnergy 每個能量盒子初始含有的能量值
	  */
	 public EnergySystem(int n, double initialEnergy){
		 energyBoxes = new double[n];
		 for (int i = 0; i < energyBoxes.length; i++)
			 energyBoxes[i] = initialEnergy;
	 }
	 
	 /**
	  * 能量的轉移,從一個盒子到另一個盒子
	  * @param from 能量源
	  * @param to     能量終點 
	  * @param amount 能量值
	  */
	 public void transfer(int from, int to, double amount){
		 
		 synchronized(lockObj){
			 
//			 if (energyBoxes[from] < amount)
//				 return;
			//while迴圈,保證條件不滿足時任務都會被條件阻擋
			 //而不是繼續競爭CPU資源
			 while (energyBoxes[from] < amount){
				 try {
					//條件不滿足, 將當前執行緒放入Wait Set
					lockObj.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			 }
			 System.out.print(Thread.currentThread().getName());
			 energyBoxes[from] -= amount;
			 System.out.printf("從%d轉移%10.2f單位能量到%d", from, amount, to);
			 energyBoxes[to] += amount;
			 System.out.printf(" 能量總和:%10.2f%n", getTotalEnergies());
			//喚醒所有在lockObj物件上等待的執行緒
			 lockObj.notifyAll();
		 }
		 
	 }
	 
	 /**
	  * 獲取能量世界的能量總和 
	  */
	 public double getTotalEnergies(){
		 double sum = 0;
		 for (double amount : energyBoxes)
			 sum += amount;
		 return sum;
	 }
	 
	 /**
	  * 返回能量盒子的長度
	  */
	 public  int getBoxAmount(){
		 return energyBoxes.length;
	 }
	 
}