358、Java中級13 -【多線程 - 死鎖、交互】 2020.06.26

1、死鎖

當業務比較複雜,多線程應用裏有可能會發生死鎖

在這裏插入圖片描述

  1. 線程1 首先佔有對象1,接着試圖佔有對象2
  2. 線程2 首先佔有對象2,接着試圖佔有對象1
  3. 線程1 等待線程2釋放對象2
  4. 與此同時,線程2等待線程1釋放對象1
    就會。。。一直等待下去,直到天荒地老,海枯石爛,山無棱 ,天地合。。。
package multiplethread;
   
import charactor.Hero;
    
public class TestThread {
      
    public static void main(String[] args) {
        final Hero ahri = new Hero();
        ahri.name = "九尾妖狐";
        final Hero annie = new Hero();
        annie.name = "安妮";
         
        Thread t1 = new Thread(){
            public void run(){
                //佔有九尾妖狐
                synchronized (ahri) {
                    System.out.println("t1 已佔有九尾妖狐");
                    try {
                        //停頓1000毫秒,另一個線程有足夠的時間佔有安妮
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                     
                    System.out.println("t1 試圖佔有安妮");
                    System.out.println("t1 等待中 。。。。");
                    synchronized (annie) {
                        System.out.println("do something");
                    }
                }  
                 
            }
        };
        t1.start();
        Thread t2 = new Thread(){
            public void run(){
                //佔有安妮
                synchronized (annie) {
                    System.out.println("t2 已佔有安妮");
                    try {
                         
                        //停頓1000毫秒,另一個線程有足夠的時間佔有暫用九尾妖狐
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    System.out.println("t2 試圖佔有九尾妖狐");
                    System.out.println("t2 等待中 。。。。");
                    synchronized (ahri) {
                        System.out.println("do something");
                    }
                }  
                 
            }
        };
        t2.start();
   }
        
}

2、JAVA 線程之間的交互 WAIT和NOTIFY

線程之間有交互通知的需求,考慮如下情況:
有兩個線程,處理同一個英雄。
一個加血,一個減血。

減血的線程,發現血量=1,就停止減血,直到加血的線程爲英雄加了血,纔可以繼續減血

3、不好的解決方式

故意設計減血線程頻率更高,蓋倫的血量遲早會到達1
減血線程中使用while循環判斷是否是1,如果是1就不停的循環,直到加血線程回覆了血量
這是不好的解決方式,因爲會大量佔用CPU,拖慢性能

  • Hero.java
package charactor;
   
public class Hero{
    public String name;
    public float hp;
      
    public int damage;
      
    public synchronized void recover(){
        hp=hp+1;
    }    
 
    public synchronized void hurt(){
            hp=hp-1;   
    }
      
    public void attackHero(Hero h) {
        h.hp-=damage;
        System.out.format("%s 正在攻擊 %s, %s的血變成了 %.0f%n",name,h.name,h.name,h.hp);
        if(h.isDead())
            System.out.println(h.name +"死了!");
    }
   
    public boolean isDead() {
        return 0>=hp?true:false;
    }
   
}
  • TestThread.java
package multiplethread;
    
import java.awt.GradientPaint;
  
import charactor.Hero;
    
public class TestThread {
    
    public static void main(String[] args) {
  
        final Hero gareen = new Hero();
        gareen.name = "蓋倫";
        gareen.hp = 616;
           
        Thread t1 = new Thread(){
            public void run(){
                while(true){
                     
                    //因爲減血更快,所以蓋倫的血量遲早會到達1
                    //使用while循環判斷是否是1,如果是1就不停的循環
                    //直到加血線程恢復了血量
                    while(gareen.hp==1){
                        continue;
                    }
                     
                    gareen.hurt();
                    System.out.printf("t1 爲%s 減血1點,減少血後,%s的血量是%.0f%n",gareen.name,gareen.name,gareen.hp);
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
 
            }
        };
        t1.start();
 
        Thread t2 = new Thread(){
            public void run(){
                while(true){
                    gareen.recover();
                    System.out.printf("t2 爲%s 回血1點,增加血後,%s的血量是%.0f%n",gareen.name,gareen.name,gareen.hp);
 
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
 
            }
        };
        t2.start();
           
    }
        
}

4、使用wait和notify進行線程交互

在Hero類中:hurt()減血方法:當hp=1的時候,執行this.wait()。
this.wait(): 表示 讓佔有this的線程等待,並臨時釋放佔有
進入hurt方法的線程必然是減血線程,this.wait()會讓減血線程臨時釋放對this的佔有。 這樣加血線程,就有機會進入recover()加血方法了。

recover() 加血方法:增加了血量,執行this.notify();
this.notify() 表示通知那些等待在this的線程,可以甦醒過來了。 等待在this的線程,恰恰就是減血線程。 一旦recover()結束, 加血線程釋放了this,減血線程,就可以重新佔有this,並執行後面的減血工作。

在這裏插入圖片描述

  • Hero.java
package charactor;
 
public class Hero {
    public String name;
    public float hp;
 
    public int damage;
 
    public synchronized void recover() {
        hp = hp + 1;
        System.out.printf("%s 回血1點,增加血後,%s的血量是%.0f%n", name, name, hp);
        // 通知那些等待在this對象上的線程,可以醒過來了,如第20行,等待着的減血線程,甦醒過來
        this.notify();
    }
 
    public synchronized void hurt() {
        if (hp == 1) {
            try {
                // 讓佔有this的減血線程,暫時釋放對this的佔有,並等待
                this.wait();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
 
        hp = hp - 1;
        System.out.printf("%s 減血1點,減少血後,%s的血量是%.0f%n", name, name, hp);
    }
 
    public void attackHero(Hero h) {
        h.hp -= damage;
        System.out.format("%s 正在攻擊 %s, %s的血變成了 %.0f%n", name, h.name, h.name, h.hp);
        if (h.isDead())
            System.out.println(h.name + "死了!");
    }
 
    public boolean isDead() {
        return 0 >= hp ? true : false;
    }
 
}
  • TestThread.java
package charactor;
 
public class Hero {
    public String name;
    public float hp;
 
    public int damage;
 
    public synchronized void recover() {
        hp = hp + 1;
        System.out.printf("%s 回血1點,增加血後,%s的血量是%.0f%n", name, name, hp);
        // 通知那些等待在this對象上的線程,可以醒過來了,如第20行,等待着的減血線程,甦醒過來
        this.notify();
    }
 
    public synchronized void hurt() {
        if (hp == 1) {
            try {
                // 讓佔有this的減血線程,暫時釋放對this的佔有,並等待
                this.wait();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
 
        hp = hp - 1;
        System.out.printf("%s 減血1點,減少血後,%s的血量是%.0f%n", name, name, hp);
    }
 
    public void attackHero(Hero h) {
        h.hp -= damage;
        System.out.format("%s 正在攻擊 %s, %s的血變成了 %.0f%n", name, h.name, h.name, h.hp);
        if (h.isDead())
            System.out.println(h.name + "死了!");
    }
 
    public boolean isDead() {
        return 0 >= hp ? true : false;
    }
 
}

5、關於wait、notify和notifyAll

留意wait()和notify() 這兩個方法是什麼對象上的?

public synchronized void hurt() {
  。。。
  this.wait();
  。。。
}
 

 
public synchronized void recover() {
   。。。
   this.notify();
}

這裏需要強調的是,wait方法和notify方法,並不是Thread線程上的方法,它們是Object上的方法。

因爲所有的Object都可以被用來作爲同步對象,所以準確的講,wait和notify是同步對象上的方法。

wait()的意思是: 讓佔用了這個同步對象的線程,臨時釋放當前的佔用,並且等待。 所以調用wait是有前提條件的,一定是在synchronized塊裏,否則就會出錯。

notify() 的意思是,通知一個等待在這個同步對象上的線程,你可以甦醒過來了,有機會重新佔用當前對象了。

notifyAll() 的意思是,通知所有的等待在這個同步對象上的線程,你們可以甦醒過來了,有機會重新佔用當前對象了。

6、參考鏈接

[01] How2j - 多線程 - 死鎖
[02] How2j - 多線程 - 交互

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