[TOC]
> 多線程的同步問題指的是多個線程同時修改一個數據的時候,可能導致的問題
> 多線程的問題,又叫Concurrency 問題
# 演示同步問題
假設蓋倫有10000滴血,并且在基地里,同時又被對方多個英雄攻擊
就是 **有多個線程在減少蓋倫的hp**
同時又有 **多個線程在恢復蓋倫的hp**
假設線程的數量是一樣的,并且每次改變的值都是1,那么所有線程結束后,蓋倫應該還是10000滴血。
但是。。。
> 注意: 不是每一次運行都會看到錯誤的數據產生,多運行幾次,或者增加運行的次數
```
package multiplethread;
import charactor.Hero4;
public class TestThread8 {
public static void main(String[] args) {
Hero4 garen = new Hero4();
garen.name = "蓋倫";
garen.hp = 10000;
System.out.printf("蓋倫的初始血量是 %.0f%n", garen.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() {
garen.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() {
garen.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, garen.hp);
}
}
```
# 分析同步問題產生的原因
1. 假設**增加線程**先進入,得到的hp是10000
2. 進行增加運算
3. 正在做增加運算的時候,**還沒有來得及修改hp的值,減少線程**來了
4. 減少線程得到的hp的值也是10000
5. 減少線程進行減少運算
6. 增加線程運算結束,得到值10001,并把這個值賦予hp
7. 減少線程也運算結束,得到值9999,并把這個值賦予hp
hp,最后的值就是9999
雖然經歷了兩個線程各自增減了一次,本來期望還是原值10000,但是卻得到了一個9999
這個時候的值9999是一個錯誤的值,在業務上又叫做**臟數據**

# 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 TestThread10 {
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();
}
}
```
# 使用synchronized 解決同步問題
所有需要修改hp的地方,有要**建立在占有someObject的基礎上**。
而對象 someObject在同一時間,只能被一個線程占有。 間接地,**導致同一時間,hp只能被一個線程修改**。
```
package multiplethread;
import charactor.Hero4;
public class TestThread11 {
public static void main(String[] args) {
Object someObject = new Object();
Hero4 garen = new Hero4();
garen.name = "蓋倫";
garen.hp = 10000;
System.out.printf("蓋倫的初始血量是 %.0f%n", garen.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() {
// 任何線程要修改hp的值,必須先占用someObject
synchronized (someObject) {
garen.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) {
garen.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, garen.hp);
}
}
```
# 使用hero對象作為同步對象
既然任意對象都可以用來作為同步對象,而所有的線程訪問的都是同一個hero對象,**索性就使用garen來作為同步對象**
進一步的,對于Hero的hurt方法,加上:
```
synchronized (this) {
}
```
表示當期對象為同步對象,即也是garen為同步對象
```
package multiplethread;
import charactor.Hero5;
public class TestThread12 {
public static void main(String[] args) {
Hero5 garen = new Hero5();
garen.name = "蓋倫";
garen.hp = 10000;
System.out.printf("蓋倫的初始血量是 %.0f%n", garen.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() {
// 使用garen作為synchronized
synchronized (garen) {
garen.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() {
// 使用garen作為synchronized
// 在方法hurt中有synchronized(this)
garen.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, garen.hp);
}
}
```
# 在方法前,加上修飾符synchronized
在recover前,直接加上synchronized ,其所對應的同步對象,就是this
和hurt方法達到的效果是一樣
外部線程訪問garen的方法,就不需要額外使用synchronized 了
```
package multiplethread;
import charactor.Hero6;
public class TestThread13 {
public static void main(String[] args) {
Hero6 garen = new Hero6();
garen.name = "蓋倫";
garen.hp = 10000;
System.out.printf("蓋倫的初始血量是 %.0f%n", garen.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() {
// recover自帶synchronized
garen.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
garen.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, garen.hp);
}
}
```
# 線程安全的類
如果一個類,其**方法都是有synchronized修飾的**,那么該類就叫做**線程安全的類
**
同一時間,只有一個線程能夠進入 **這種類的一個實例** 的去修改數據,進而保證了這個實例中的數據的安全(不會同時被多線程修改而變成臟數據)
比如StringBuffer和StringBuilder的區別
StringBuffer的方法都是有synchronized修飾的,StringBuffer就叫做線程安全的類
而StringBuilder就不是線程安全的類
```
```