356、Java中級11 -【多線程 - 同步】 2020.06.26

1、 同步

多線程的同步問題指的是多個線程同時修改一個數據的時候,可能導致的問題。
多線程的問題,又叫Concurrency 問題。

2、演示同步問題

假設蓋倫有10000滴血,並且在基地裏,同時又被對方多個英雄攻擊
就是有多個線程在減少蓋倫的hp
同時又有多個線程在恢復蓋倫的hp
假設線程的數量是一樣的,並且每次改變的值都是1,那麼所有線程結束後,蓋倫應該還是10000滴血。

但是:。。。

注意: 不是每一次運行都會看到錯誤的數據產生,多運行幾次,或者增加運行的次數

在這裏插入圖片描述

  • Hello.java
package charactor;
  
public class Hero{
    public String name;
    public float hp;
     
    public int damage;
     
    //回血
    public void recover(){
        hp=hp+1;
    }
     
    //掉血
    public 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 charactor.Hero;
   
public class TestThread {
   
    public static void main(String[] args) {
           
        final Hero gareen = new Hero();
        gareen.name = "蓋倫";
        gareen.hp = 10000;
          
        System.out.printf("蓋倫的初始血量是 %.0f%n", gareen.hp);
          
        //多線程同步問題指的是多個線程同時修改一個數據的時候,導致的問題
          
        //假設蓋倫有10000滴血,並且在基地裏,同時又被對方多個英雄攻擊
          
        //用JAVA代碼來表示,就是有多個線程在減少蓋倫的hp
        //同時又有多個線程在恢復蓋倫的hp
          
        //n個線程增加蓋倫的hp
          
        int n = 10000;
  
        Thread[] addThreads = new Thread[n];
        Thread[] reduceThreads = new Thread[n];
          
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(){
                public void run(){
                    gareen.recover();
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            };
            t.start();
            addThreads[i] = t;
              
        }
          
        //n個線程減少蓋倫的hp
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(){
                public void run(){
                    gareen.hurt();
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            };
            t.start();
            reduceThreads[i] = t;
        }
          
        //等待所有增加線程結束
        for (Thread t : addThreads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        //等待所有減少線程結束
        for (Thread t : reduceThreads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
          
        //代碼執行到這裏,所有增加和減少線程都結束了
          
        //增加和減少線程的數量是一樣的,每次都增加,減少1.
        //那麼所有線程都結束後,蓋倫的hp應該還是初始值
          
        //但是事實上觀察到的是:
                  
        System.out.printf("%d個增加線程和%d個減少線程結束後%n蓋倫的血量變成了 %.0f%n", n,n,gareen.hp);
          
    }
       
}

3、分析同步問題產生的原因

  1. 假設增加線程先進入,得到的hp是10000
  2. 進行增加運算
  3. 正在做增加運算的時候,還沒有來得及修改hp的值,減少線程來了
  4. 減少線程得到的hp的值也是10000
  5. 減少線程進行減少運算
  6. 增加線程運算結束,得到值10001,並把這個值賦予hp
  7. 減少線程也運算結束,得到值9999,並把這個值賦予hp
    hp,最後的值就是9999
    雖然經歷了兩個線程各自增減了一次,本來期望還是原值10000,但是卻得到了一個9999
    這個時候的值9999是一個錯誤的值,在業務上又叫做髒數據

3、解決思路

總體解決思路是: 在增加線程訪問hp期間,其他線程不可以訪問hp

  1. 增加線程獲取到hp的值,並進行運算
  2. 在運算期間,減少線程試圖來獲取hp的值,但是不被允許
  3. 增加線程運算結束,併成功修改hp的值爲10001
  4. 減少線程,在增加線程做完後,才能訪問hp的值,即10001
  5. 減少線程運算,並得到新的值10000

在這裏插入圖片描述

4、synchronized 同步對象概念

解決上述問題之前,先理解
synchronized 關鍵字的意義
如下代碼:

Object someObject =new Object();
synchronized (someObject){
  //此處的代碼只有佔有了someObject後纔可以執行
}

synchronized表示當前線程,獨佔 對象 someObject

當前線程獨佔 了對象someObject,如果有其他線程試圖佔有對象someObject,就會等待,直到當前線程釋放對someObject的佔用。
someObject 又叫同步對象,所有的對象,都可以作爲同步對象

爲了達到同步的效果,必須使用同一個同步對象

釋放同步對象的方式: synchronized 塊自然結束,或者有異常拋出

在這裏插入圖片描述

package multiplethread;
 
import java.text.SimpleDateFormat;
import java.util.Date;
  
public class TestThread {
	
	public static String now(){
		return new SimpleDateFormat("HH:mm:ss").format(new Date());
	}
	
    public static void main(String[] args) {
        final Object someObject = new Object();
         
        Thread t1 = new Thread(){
            public void run(){
                try {
                    System.out.println( now()+" t1 線程已經運行");
                    System.out.println( now()+this.getName()+ " 試圖佔有對象:someObject");
                    synchronized (someObject) {
                         
                        System.out.println( now()+this.getName()+ " 佔有對象:someObject");
                        Thread.sleep(5000);
                        System.out.println( now()+this.getName()+ " 釋放對象:someObject");
                    }
                    System.out.println(now()+" t1 線程結束");
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        };
        t1.setName(" t1");
        t1.start();
        Thread t2 = new Thread(){
 
            public void run(){
                try {
                    System.out.println( now()+" t2 線程已經運行");
                    System.out.println( now()+this.getName()+ " 試圖佔有對象:someObject");
                    synchronized (someObject) {
                        System.out.println( now()+this.getName()+ " 佔有對象:someObject");
                        Thread.sleep(5000);
                        System.out.println( now()+this.getName()+ " 釋放對象:someObject");
                    }
                    System.out.println(now()+" t2 線程結束");
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        };
        t2.setName(" t2");
        t2.start();
    }
      
}

5、使用synchronized 解決同步問題 頂 折

所有需要修改hp的地方,有要建立在佔有someObject的基礎上。
而對象 someObject在同一時間,只能被一個線程佔有。 間接地,導致同一時間,hp只能被一個線程修改。

在這裏插入圖片描述

package multiplethread;
   
import java.awt.GradientPaint;
 
import charactor.Hero;
   
public class TestThread {
   
    public static void main(String[] args) {
 
        final Object someObject = new Object();
         
        final Hero gareen = new Hero();
        gareen.name = "蓋倫";
        gareen.hp = 10000;
          
        int n = 10000;
  
        Thread[] addThreads = new Thread[n];
        Thread[] reduceThreads = new Thread[n];
          
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(){
                public void run(){
                     
                    //任何線程要修改hp的值,必須先佔用someObject
                    synchronized (someObject) {
                        gareen.recover();
                    }
                     
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            };
            t.start();
            addThreads[i] = t;
              
        }
          
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(){
                public void run(){
                    //任何線程要修改hp的值,必須先佔用someObject
                    synchronized (someObject) {
                        gareen.hurt();
                    }
                     
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            };
            t.start();
            reduceThreads[i] = t;
        }
          
        for (Thread t : addThreads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        for (Thread t : reduceThreads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
          
        System.out.printf("%d個增加線程和%d個減少線程結束後%n蓋倫的血量是 %.0f%n", n,n,gareen.hp);
          
    }
       
}

6、使用hero對象作爲同步對象 頂 折

既然任意對象都可以用來作爲同步對象,而所有的線程訪問的都是同一個hero對象,索性就使用gareen來作爲同步對象
進一步的,對於Hero的hurt方法,加上:
synchronized (this) {
}
表示當前對象爲同步對象,即也是gareen爲同步對象

  • Hero.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 = 10000;
          
        int n = 10000;
  
        Thread[] addThreads = new Thread[n];
        Thread[] reduceThreads = new Thread[n];
          
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(){
                public void run(){
                     
                    //使用gareen作爲synchronized
                    synchronized (gareen) {
                        gareen.recover();
                    }
                     
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            };
            t.start();
            addThreads[i] = t;
              
        }
          
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(){
                public void run(){
                    //使用gareen作爲synchronized
                    //在方法hurt中有synchronized(this)
                    gareen.hurt();
                     
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            };
            t.start();
            reduceThreads[i] = t;
        }
          
        for (Thread t : addThreads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        for (Thread t : reduceThreads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
          
        System.out.printf("%d個增加線程和%d個減少線程結束後%n蓋倫的血量是 %.0f%n", n,n,gareen.hp);
          
    }
       
}
  • TestThread.java
package charactor;
  
public class Hero{
    public String name;
    public float hp;
     
    public int damage;
     
    //回血
    public void recover(){
        hp=hp+1;
    }
     
    //掉血
    public void hurt(){
        //使用this作爲同步對象
        synchronized (this) {
            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;
    }
  
}

7、在方法前,加上修飾符synchronized

在recover前,直接加上synchronized ,其所對應的同步對象,就是this
和hurt方法達到的效果是一樣
外部線程訪問gareen的方法,就不需要額外使用synchronized 了

  • Hero.java
package charactor;
  
public class Hero{
    public String name;
    public float hp;
     
    public int damage;
     
    //回血
    //直接在方法前加上修飾符synchronized
    //其所對應的同步對象,就是this
    //和hurt方法達到的效果一樣
    public synchronized void recover(){
        hp=hp+1;
    }
     
    //掉血
    public void hurt(){
        //使用this作爲同步對象
        synchronized (this) {
            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 = 10000;
          
        int n = 10000;
  
        Thread[] addThreads = new Thread[n];
        Thread[] reduceThreads = new Thread[n];
          
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(){
                public void run(){
                     
                    //recover自帶synchronized
                    gareen.recover();
                     
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            };
            t.start();
            addThreads[i] = t;
              
        }
          
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(){
                public void run(){
                    //hurt自帶synchronized
                    gareen.hurt();
                     
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            };
            t.start();
            reduceThreads[i] = t;
        }
          
        for (Thread t : addThreads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        for (Thread t : reduceThreads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
          
        System.out.printf("%d個增加線程和%d個減少線程結束後%n蓋倫的血量是 %.0f%n", n,n,gareen.hp);
          
    }
       
}

8、線程安全的類

如果一個類,其方法都是有synchronized修飾的,那麼該類就叫做線程安全的類

同一時間,只有一個線程能夠進入 這種類的一個實例 的去修改數據,進而保證了這個實例中的數據的安全(不會同時被多線程修改而變成髒數據)

比如StringBuffer和StringBuilder的區別
StringBuffer的方法都是有synchronized修飾的,StringBuffer就叫做線程安全的類
而StringBuilder就不是線程安全的類

在這裏插入圖片描述

9、參考鏈接

[01] How2j - 多線程 - 同步

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