Java 多線程同步 synchronized 多線程的同步問題指的是多個線程同時修改一個數據的時候,可能導致的問題 多線程的問題,又叫 Concurrency 問題 步驟 1 : 演示同步問題 假設蓋倫有10000滴血,並且在基地里,同時又被對方多個英雄攻擊 就是有 多個線程在減少蓋倫的hp 同時 ...
Java 多線程同步 synchronized
多線程的同步問題指的是多個線程同時修改一個數據的時候,可能導致的問題
多線程的問題,又叫Concurrency 問題
步驟 1 : 演示同步問題
假設蓋倫有10000滴血,並且在基地里,同時又被對方多個英雄攻擊
就是有多個線程在減少蓋倫的hp
同時又有多個線程在恢覆蓋倫的hp
假設線程的數量是一樣的,並且每次改變的值都是1,那麼所有線程結束後,蓋倫應該還是10000滴血。
但是。。。
註意: 不是每一次運行都會看到錯誤的數據產生,多運行幾次,或者增加運行的次數
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;
}
}
.
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);
}
}
步驟 2 : 分析同步問題產生的原因
- 假設增加線程先進入,得到的hp是10000
- 進行增加運算
- 正在做增加運算的時候,還沒有來得及修改hp的值,減少線程來了
- 減少線程得到的hp的值也是10000
- 減少線程進行減少運算
- 增加線程運算結束,得到值10001,並把這個值賦予hp
- 減少線程也運算結束,得到值9999,並把這個值賦予hp
hp,最後的值就是9999
雖然經歷了兩個線程各自增減了一次,本來期望還是原值10000,但是卻得到了一個9999
這個時候的值9999是一個錯誤的值,在業務上又叫做臟數據
步驟 3 : 解決思路
總體解決思路是: 在增加線程訪問hp期間,其他線程不可以訪問hp
- 增加線程獲取到hp的值,併進行運算
- 在運算期間,減少線程試圖來獲取hp的值,但是不被允許
- 增加線程運算結束,併成功修改hp的值為10001
- 減少線程,在增加線程做完後,才能訪問hp的值,即10001
- 減少線程運算,並得到新的值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為同步對象
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);
}
}
.
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 了
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;
}
}
.
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就不是線程安全的類