Java多線程編程(同步、死鎖、生產消費):
關於線程同步以及死鎖問題:
線程同步概念:是指若干個線程對象並行進行資源的訪問時實現的資源處理保護操作;
線程死鎖概念:是指兩個線程都在等待對方先完成,造成程序的停止的狀態;
先了解相應的概念,后面深入理解。
同步:
舉個例子:還是賣票問題(經典❗)
- 不存在同步
- 開啟三個線程(售票員)測試
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 票已經賣光了 ***** |
存在上述原因是因為在代碼中兩個地方存在多線程訪問時出現模糊的問題:
- this.ticket>0;
- this,ticket--;
假設現在剩余的票數為1張;當第一個線程滿足售票的條件的時候(此時還未減少票數),其他的線程也可能同時滿足售票的條件,這樣同時進行自減減就可能造成負數!
解決上述問題就需要采用線程同步技術實現;
首先需要明確,在Java中實現線程同步(synchronized)的方法有兩個:
-
同步代碼塊(同步策略加在方法內部)
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 ***** 票已經賣光了 ***** ***** 票已經賣光了 ***** ***** 票已經賣光了 *****
-
同步方法(同步策略加在方法上)
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 ***** 票已經賣光了 ***** ***** 票已經賣光了 ***** ***** 票已經賣光了 *****
同步的本質:在同一個時間段只允許有一個線程執行資源,所以在此線程對象未執行完的過程中其他線程對象將處於等待的狀態。
同步的優點與缺點:
-
可以保證數據的准確性
-
數據線程的訪問安全
-
程序的處理性能下降
死鎖:
實例:
假如現在又張三想要李四的畫,李四想要張三的書,那么張三對李四說:把你的畫給我,我就給你書;
李四對張三說:把你的書給我,我就給你畫;
此時:張三在等待李四,李四在等待張三,兩人一直等待下去形成死鎖;
觀察線程的死鎖:(實現張三李四)
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() ;
}
}
由於死鎖是不可預測發生的,該代碼有可能在一次運行中展示不出效果來,需要多次運行觀察效果;
效果圖:
由此引申出了生產者與消費者模型。
生產者與消費者問題:
首先需要明確生產者與消費者為兩個線程對象,是對同一資源進行數據的保存與讀取;
基本操作是:生產者生產一個資源,消費者則取走一個資源,一一對應。
對應類關系圖:
我們需要設想一個問題,如果不加任何操作的話,會出現什么問題?
- 數據錯位:當生產者線程只是開辟了一個棧空間保存信息名稱,在想存數據但是還沒存數據的時候切換到了消費者線程上,那么消費者線程將會把這個信息名稱與上個信息的內容進行結合聯系,這樣就造成了數據的錯位。
- 重復數據:當生產者放了若干次的數據,消費者才開始取數據,或者消費者取完,但生產者還沒生產新數據時又取了直接已經取過得數據。
解決以上兩個問題需要涉及到以下兩個知識點:
-
設置同步代碼塊或設置同步方法>>>解決數據錯誤問題
-
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來完成,如果想要讓生產者與消費者一個一個拿,一個一個取,那么需要加入標志位來確定線程的當前狀態;
由圖所示:
當生產者線程與消費者線程進入時,判斷當前的標志位是否為true,
-
true:表示生產者可以生產資源,但是消費者不能取走資源
-
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(); // 喚醒等待線程
}
}
}
在本程序中追加一個數據產生與消費的控制邏輯成員屬性,通過此程序的值控制實現線程的等待與喚醒處理操作,從而解決線程重復操作的問題。