多執行緒併發安全問題詳解

帥成一匹馬發表於2019-03-24

定義:

      當多個執行緒同時執行,多個執行緒之間是相互搶佔資源執行,並且搶佔是發生線上程的執行的每一步過程中,導致出現非法資料。這種現象就稱之為多執行緒的併發安全問題。

程式碼案例:

public class SellTicketDemo {
	  public static void main(String[] args) {
		//建立票物件
		Ticket t=new Ticket();
		//給票的count屬性賦值為100
		t.setCount(100);
		
		SellSystem s=new SellSystem(t);
		//4個執行緒對應4個售票視窗
		Thread thread1=new Thread(s);
		Thread thread2=new Thread(s);
		Thread thread3=new Thread(s);
		Thread thread4=new Thread(s);
		thread1.start();
		thread2.start();
		thread3.start();
		thread4.start();
	}
}

//賣票系統
class SellSystem implements Runnable{
	//定義票物件t
	private Ticket t;
	
	public SellSystem(Ticket t) {
		this.t=t;
	}
	@Override
	public void run() {
		while(true){
			try {
				//此處讓進來的執行緒睡一會是為了增加其他執行緒搶佔到執行權的概率
				Thread.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			if (t.getCount()>0) {
				t.setCount(t.getCount()-1);
				//Thread.currentThread()表示獲取當前正在執行的執行緒,t.getCount()是t.setCount設定後的數量
				System.out.println(Thread.currentThread().getName()+"賣了一張票,還剩"+t.getCount());
			}
		}
	}
}

//票
class Ticket{
	//定義票的數量屬性
	private int count;

	public int getCount() {
		return count;
	}

	public void setCount(int count) {
		this.count = count;
	}
	
}

執行結果:

異常解析:

 上述出現了三種異常情況:1. 跳過數字  2. 重複數字  3. 出現負數

  1. 跳過數字 :當票還剩9張時,假設小紅執行緒先搶到執行權,進入第①步,經過getCount得到9,9>0進入第②步,經過計算再用setCount將8設定為結果值,此時搶到了執行權,由於小紅已經setCount設定了結果值,小蘭進入第①步後通過getCount得到8,8>0進入第②步,經過計算以及setCount將結果值設定為7,進入第③步,列印出7執行完畢。這時小紅又搶回執行權,由於總票數是大家共享的,此時的總票數為7,小紅前兩步已完成,直接進入第③步,經過getCount得到7,然後列印出7執行完畢。因此會出現9 , 7, 7 跳過數字的情況。

2.重複數字當票還剩4張時,假設小紅執行緒先搶到執行權,進入第①步,經過getCount得到5,5>0進入第②步,經過(t.getCount() - 1)計算變為3,正當小紅準備setCount將3設定為結果值時,搶到了執行權,由於小紅還沒有setCount設定結果值,小蘭進入第①步後通過getCount得到的還是4,4>0進入第②步,經過計算再用setCount將結果值設定為3,進入第③步,列印出3執行完畢。這時小紅又搶回執行權,由於總票數是大家共享的,此時的總票數為3,小紅接著上次未執行完的操作直接setCount,將3設定為結果值,接著進入第③步,列印出3執行完畢。因此會出現3 ,3 重複數字的情況。

3.出現負數當票還剩1張時,假設小紅執行緒先搶到執行權,進入第①步,經過getCount得到1,1>0準備進入第②步,此時小蘭搶到了執行權,進入第①步,經過getCount仍然得到1,1>0準備進入第②步,此時又被小黑搶到了執行權,進入第①步,經過getCount仍然得到1,1>0進入第②步,經過計算以及setCount將0設為結果值,進入第③步,列印出0執行完畢。此時小紅又搶到執行權,由於小紅上次已經執行完第①步,直接進入第②步,經過getCount得到0,再減1,最後由setCount將-1設定為結果值,進入第③步,列印出-1執行完畢。此時小蘭又搶到執行權,由於小蘭上次已經執行完第①步,直接進入第②步,經過getCount得到-1,再減1,最後由setCount將-2設定為結果值,進入第③步,列印出-2執行完畢。因此會出現0, -1 , -2 負數的情況。

4. 解決方法:

通過同步程式碼塊的方式將物件鎖起來。用synchronized來將程式碼限制起來,需要鎖物件。鎖物件要求所有的執行緒都得認識。鎖物件:共享資源、方法區中的資料、this。

正確程式碼:

修改的程式碼如下:

synchronized (t) {
    if (t.getCount()>0) {
	    t.setCount(t.getCount()-1);
	    //Thread.currentThread()表示獲取當前正在執行的執行緒,t.getCount()是t.setCount設定後數量
          System.out.println(Thread.currentThread().getName()+"賣了一張票,還剩"+t.getCount());
	}
}

最終程式碼如下:

public class SellTicketDemo {
	  public static void main(String[] args) {
		//建立票物件
		Ticket t=new Ticket();
		//給票的count屬性賦值為100
		t.setCount(100);
		
		SellSystem s=new SellSystem(t);
		
		//4個執行緒對應4個售票視窗
		Thread thread1=new Thread(s);
		Thread thread2=new Thread(s);
		Thread thread3=new Thread(s);
		Thread thread4=new Thread(s);
		thread1.start();
		thread2.start();
		thread3.start();
		thread4.start();
	}
}

//賣票系統
class SellSystem implements Runnable{
	//定義票物件t
	private Ticket t;
	
	public SellSystem(Ticket t) {
		this.t=t;
	}
	@Override
	public void run() {
		while(true){
			try {
				//此處讓進來的執行緒睡一會是為了增加其他執行緒搶佔到執行權的概率
				Thread.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			synchronized (t) {
				if (t.getCount()>0) {
					t.setCount(t.getCount()-1);
					//Thread.currentThread()表示獲取當前正在執行的執行緒,t.getCount()是t.setCount設定後的數量
					System.out.println(Thread.currentThread().getName()+"賣了一張票,還剩"+t.getCount());
				}
			}
		}
	}
}

//票
class Ticket{
	//定義票的數量屬性
	private int count;

	public int getCount() {
		return count;
	}

	public void setCount(int count) {
		this.count = count;
	}
	
}

執行結果:

           

 

相關文章