Java多執行緒程式設計(同步、死鎖、生產消費者問題)

xbhog發表於2021-04-22

Java多執行緒程式設計(同步、死鎖、生產消費):

關於執行緒同步以及死鎖問題:

執行緒同步概念:是指若干個執行緒物件並行進行資源的訪問時實現的資源處理保護操作;

執行緒死鎖概念:是指兩個執行緒都在等待對方先完成,造成程式的停止的狀態;

先了解相應的概念,後面深入理解。

同步:

舉個例子:還是賣票問題(經典❗)

  1. 不存在同步
  2. 開啟三個執行緒(售票員)測試
package com.xbhog;
class MyThread implements Runnable {// 定義執行緒執行類
    private int ticket = 3;// 總票數為6張
    @Override
    public void run() {
        while (true) {	// 持續賣票
            if (this.ticket > 0) {	// 還有剩餘票
                try {
                    Thread.sleep(100);	// 模擬網路延遲
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //獲取當前執行緒的名字
                System.out.println(Thread.currentThread().getName() +
                        "賣票,ticket = " + this.ticket--);
            } else {
                System.out.println("***** 票已經賣光了 *****");
                break;// 跳出迴圈
            }
        }
    }
}
public class Java多執行緒核心 {
    public static void main(String[] args) throws Exception {
        MyThread mt = new MyThread();
        new Thread(mt, "售票員A").start();	// 開啟賣票執行緒
        new Thread(mt, "售票員B").start();	// 開啟賣票執行緒
        new Thread(mt, "售票員C").start();	// 開啟賣票執行緒
    }
}

結果:

第一次隨機執行: 第二次隨機執行:
售票員B賣票,ticket = 2
售票員C賣票,ticket = 3
售票員A賣票,ticket = 3
售票員A賣票,ticket = 1
售票員B賣票,ticket = -1
***** 票已經賣光了
售票員C賣票,ticket = 0
票已經賣光了
票已經賣光了 *****
售票員B賣票,ticket = 1
***** 票已經賣光了
售票員A賣票,ticket = 3
票已經賣光了
售票員C賣票,ticket = 2
票已經賣光了 *****

存在上述原因是因為在程式碼中兩個地方存在多執行緒訪問時出現模糊的問題:

  1. this.ticket>0;
  2. this,ticket--;

假設現在剩餘的票數為1張;當第一個執行緒滿足售票的條件的時候(此時還未減少票數),其他的執行緒也可能同時滿足售票的條件,這樣同時進行自減減就可能造成負數!

解決上述問題就需要採用執行緒同步技術實現;

首先需要明確,在Java中實現執行緒同步(synchronized)的方法有兩個:

  1. 同步程式碼塊(同步策略加在方法內部)

    package com.xbhog.多執行緒1;
    class MyThread implements Runnable {						// 定義執行緒執行類
        private int ticket = 3; 								// 總票數為6張
        @Override
        public void run() {
            while (true) {									// 持續賣票
                synchronized(this) {							// 同步程式碼塊
                    if (this.ticket > 0) {					// 還有剩餘票
                        try {
                            Thread.sleep(100);				// 模擬網路延遲
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() +
                                "賣票,ticket = " + this.ticket--);
                    } else {
                        System.out.println("***** 票已經賣光了 *****");
                        break;								// 跳出迴圈
                    }
                }
            }
        }
    }
    public class Java多執行緒同步程式碼塊 {
        public static void main(String[] args) {
            MyThread mt = new MyThread();
            new Thread(mt, "售票員A").start();					// 開啟賣票執行緒
            new Thread(mt, "售票員B").start();					// 開啟賣票執行緒
            new Thread(mt, "售票員C").start();					// 開啟賣票執行緒
        }
    }
    
    售票員A賣票,ticket = 3
    售票員C賣票,ticket = 2
    售票員B賣票,ticket = 1
    ***** 票已經賣光了 *****
    ***** 票已經賣光了 *****
    ***** 票已經賣光了 *****
    
  2. 同步方法(同步策略加在方法上)

    class MyThread implements Runnable {						// 定義執行緒執行類
    	private int ticket = 3; 								// 總票數為6張
    	@Override
    	public void run() {
    		while (this.sale()) {								// 呼叫同步方法
    			;
    		}
    	}
    	public synchronized boolean sale() {					// 售票操作
    		if (this.ticket > 0) {
    			try {
    				Thread.sleep(100); 						// 模擬網路延遲
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    			System.out.println(Thread.currentThread().getName() + 
    				"賣票,ticket = " + this.ticket--);
    			return true;
    		} else {
    			System.out.println("***** 票已經賣光了 *****");
    			return false;
    		}
    	}
    }
    public class ThreadDemo {
    	public static void main(String[] args) throws Exception {
    		MyThread mt = new MyThread();
    		new Thread(mt, "售票員A").start();					// 開啟賣票執行緒
    		new Thread(mt, "售票員B").start();					// 開啟賣票執行緒
    		new Thread(mt, "售票員C").start();					// 開啟賣票執行緒
    	}
    }
    
    售票員A賣票,ticket = 3
    售票員C賣票,ticket = 2
    售票員B賣票,ticket = 1
    ***** 票已經賣光了 *****
    ***** 票已經賣光了 *****
    ***** 票已經賣光了 *****
    

同步的本質:在同一個時間段只允許有一個執行緒執行資源,所以在此執行緒物件未執行完的過程中其他執行緒物件將處於等待的狀態。

同步的優點與缺點:

  1. 可以保證資料的準確性

  2. 資料執行緒的訪問安全


  3. 程式的處理效能下降

死鎖:

例項:

假如現在又張三想要李四的畫,李四想要張三的書,那麼張三對李四說:把你的畫給我,我就給你書;

李四對張三說:把你的書給我,我就給你畫;

此時:張三在等待李四,李四在等待張三,兩人一直等待下去形成死鎖;

觀察執行緒的死鎖:(實現張三李四)

package com.xbhog.死鎖;

class Book {
    public synchronized void tell(Painting paint) {		// 同步方法
        System.out.println("張三對李四說:把你的畫給我,我就給你書,不給畫不給書!");
        paint.get();
    }
    public synchronized void get() {						// 同步方法
        System.out.println("張三得到了李四的畫開始認真欣賞。");
    }
}
class Painting {
    public synchronized void tell(Book book) {				// 同步方法
        System.out.println("李四對張三說:把你的書給我,我就給你畫,不給書不給畫!");
        book.get();
    }
    public synchronized void get() {						// 同步方法
        System.out.println("李四得到了張三的書開始認真閱讀。");
    }
}
public class DeadLock implements Runnable{
    private Book book = new Book();
    private Painting paint = new Painting();
    public DeadLock() {
        new Thread(this).start();
        book.tell(paint);
    }
    @Override
    public void run() {
        paint.tell(book);
    }
    public static void main(String[] args) {
        new DeadLock() ;
    }
}

由於現在電腦的配置問題,該程式碼有可能在一次執行中展示不出效果來,需要多次執行觀察效果;

效果圖:

image-20210422104049966

由此引申出了生產者與消費者模型。

生產者與消費者問題:

首先需要明確生產者與消費者為兩個執行緒物件,是對同一資源進行資料的儲存與讀取;

基本操作是:生產者生產一個資源,消費者則取走一個資源,一一對應。

對應類關係圖:

image-20210421234308037

我們需要設想一個問題,如果不加任何操作的話,會出現什麼問題?

  1. 資料錯位:當生產者執行緒只是開闢了一個棧空間儲存資訊名稱,在想存資料但是還沒存資料的時候切換到了消費者執行緒上,那麼消費者執行緒將會把這個資訊名稱與上個資訊的內容進行結合聯絡,這樣就造成了資料的錯位。
  2. 重複資料:當生產者放了若干次的資料,消費者才開始取資料,或者消費者取完,但生產者還沒生產新資料時又取了直接已經取過得資料。

解決以上兩個問題需要涉及到以下兩個知識點:

  1. 設定同步程式碼塊或設定同步方法>>>解決資料錯誤問題

  2. Object執行緒等待與喚醒>>>解決資料重複設定以及重複取出的問題

增加資料同步方法或同步程式碼塊:

在本程式中,生產者與消費者代表的都是執行緒物件,所以同步操作只能在Message類中,可以將set與get方法設定為單獨的同步方法。

class Message {
	private String title ;							// 儲存資訊的標題
	private String content ;							// 儲存資訊的內容
	public synchronized void set(String title, String content) {
		this.title = title;
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		this.content = content;
	}
	public synchronized String get() {
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return this.title + " --> " + this.content;
	}
	// setter、getter略
}
class Producer implements Runnable {					// 定義生產者
	private Message msg = null ;
	public Producer(Message msg) {
		this.msg = msg ;
	}
	@Override
	public void run() {
		for (int x = 0; x < 50; x++) {				// 生產50次資料
			if (x % 2 == 0) {
				this.msg.set("xbhog","22") ;	// 設定屬性
			} else {
				this.msg.set("xbhog","www.cnblog.cn/xbhog") ;	// 設定屬性
			}
		}
	}
}
class Consumer implements Runnable {					// 定義消費者
	private Message msg = null ;
	public Consumer (Message msg) {
		this.msg = msg ;
	}
	@Override
	public void run() {
		for (int x = 0; x < 50; x++) {				// 取走50次資料
			System.out.println(this.msg.get()); 		// 取得屬性
		}
	}
}
public class ThreadDemo {
	public static void main(String[] args) throws Exception {
		Message msg = new Message() ;					// 定義Message物件,用於儲存和取出資料
		new Thread(new Producer(msg)).start() ;		// 啟動生產者執行緒
		new Thread(new Consumer(msg)).start() ;		// 取得消費者執行緒
	}
}

Object執行緒等待與喚醒機制:

執行緒的等待與喚醒只能依靠Object來完成,如果想要讓生產者與消費者一個一個拿,一個一個取,那麼需要加入標誌位來確定執行緒的當前狀態;

由圖所示:

image-20210422111820013

當生產者執行緒與消費者執行緒進入時,判斷當前的標誌位是否為true,

  1. true:表示生產者可以生產資源,但是消費者不能取走資源

  2. false:表示生產者不能生產資源,但是消費者需要取走資源

class Message {
	private String title ;
	private String content ;
	private boolean flag = true; 					// 表示生產或消費的形式
	// flag = true:允許生產,但是不允許消費
	// flag = false:允許消費,不允許生產
	public synchronized void set(String title,String content) {
		if (this.flag == false) {						// 無法進行生產,等待被消費
			try {
				super.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			} 
		}
		this.title = title ;
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		this.content = content ;
		this.flag = false ; 							// 已經生產過了
		super.notify(); 								// 喚醒等待的執行緒
	}
	public synchronized String get() {
		if (this.flag == true) {						// 還未生產,需要等待
			try {
				super.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		try {
			return this.title + "  -  " + this.content ;
		} finally {										// 不管如何都要執行
			this.flag = true ; 							// 繼續生產
			super.notify(); 								// 喚醒等待執行緒
		}
	}
}

在本程式中追加一個資料產生與消費的控制邏輯成員屬性,通過此程式的值控制實現執行緒的等待與喚醒處理操作,從而解決執行緒重複操作的問題。

相關文章