# 牛年自強計畫 Week 3 - SpringBoot - 番外篇 Java 執行緒 Synchronized/Non-Synchronized
## 【前言】
了解 **Thread** 和 **ThreadPool** 後,接著就來看使用 Thread 後會遇到的**同步/非同步問題**。
我們先看一個基本的銀行提款例子!

- 當在銀行有總額 3000 的錢,一次提領 1000 元,總計可以提領三次。
- 就算分不同人依序提領同一個帳戶,提領出的總金額仍為 3000 元。
- 若是同時提領,銀行的機器也該是正常給出總額一樣的錢。然而卻可能在程式的執行結果出現三種可能:
1. 所有人都領到了錢,錢的總額變成 5000 元,然而實際只有 3000 元,等於多出 2000 元
2. 其中一人沒有領到錢,錢的總額變成 4000 元,依然比實際的 3000 元還多 1000 元
3. 其中兩人沒有領到錢,這是符合預期的結果
這是緣由於程式的請求預設都是 **"非同步"** 的處理方式,同時間、不同的程序取得了對相同物件操作的權限,因此在操作中 **"可能"** 會取得相同的結果,但實務上 "**不應該**" 取得相同的結果,這就產生了程式邏輯與實際業務邏輯的衝突。
正確的方式是讓複數的請求,在同時間的情況下,只有一個請求可以取得對目標物件的操作權限,讓其餘四個進入等待。並在完成操作後,將操作權拋出讓另外四個去爭搶(爭搶是額外的問題了~)。
單機程式在 Thread 使用之前,程序通常都是只有一條主 Thread 在跑,不太會有方法同時被複數使用的狀況。
但對連網服務來說,處理同步、非同步問題應該是必備的技術。不是必學的觀念,是必備的技術!不要說 **C10K**,光是每秒 10 個請求,就會遇到資料處理衝突的問題了。
因此未來若想走電商或者互聯網類型的工作產業,同步、非同步的工程,資料處理演算法等等會是需要著重加強的部分。
## 【Non-Synchronized 介紹】
一般物件、方法或屬性的預設狀態,表示所有請求皆不會阻擋使用,同個資源可同時被諸多請求影響,依照請求時間與影響時間決定下一個請求取的的狀態為何。因為會導致結果極其不準確,因此在許多資料處理的狀況會去避免這種問題的產生。
### 【Non-Synchronized 範例】
**build.gradle**
```xml=
plugins {
id 'org.springframework.boot' version '2.3.3.RELEASE'
id 'io.spring.dependency-management' version '1.0.8.RELEASE'
id 'java'
}
group 'org.example'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
configurations.all {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.junit.jupiter:junit-jupiter-api:5.6.0'
implementation 'org.junit.jupiter:junit-jupiter-engine:5.6.0'
implementation 'org.springframework.boot:spring-boot-starter-log4j2'
}
test {
useJUnitPlatform()
}
```
**account.java**
```java=
package Thread.NonSynchronized;
public class account {
private int money;
private int milliSecondBase = 10;
public void withdraw(int withdrawMoney) {
try {
int totalTime = milliSecondBase * (int)(10*Math.random());
//System.out.println("This move need : " + totalTime + " secs to finish.");
Thread.sleep(totalTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
if((money - withdrawMoney) < 0) {
System.out.println("Not enough money now $ : " + money + " $");
return;
}
money -= withdrawMoney;
System.out.println("Withdraw $ : " + withdrawMoney + " $");
}
public void deposit(int depositMoney){
money += depositMoney;
System.out.println("Now have $ : " + money + " $");
}
public int getMoney(){
return this.money;
}
public void setMilliSecondBase(int newMilliSecondBase){
this.milliSecondBase = newMilliSecondBase;
}
}
```
**testAccount.java**
```java=
package Thread.NonSynchronized;
import org.junit.jupiter.api.*;
public class testAccount {
private account account = new account();
@DisplayName("Test Synchronization: Non-Static Method, 10 ms (Usually See)")
@RepeatedTest( value = 10, name="{displayName} 第{currentRepetition}次測試,總共需要執行{totalRepetitions}次 ")
public void testCase1(){
account.deposit(3000);
account.setMilliSecondBase(10);
new Thread(() -> account.withdraw(1000)).start();
new Thread(() -> account.withdraw(1000)).start();
new Thread(() -> account.withdraw(1000)).start();
new Thread(() -> account.withdraw(1000)).start();
new Thread(() -> account.withdraw(1000)).start();
try {
Thread.sleep((int)(1000*2));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@DisplayName("Test Synchronization: Non-Static Method, 100 ms (Medium See)")
@RepeatedTest( value = 10, name="{displayName} 第{currentRepetition}次測試,總共需要執行{totalRepetitions}次 ")
public void testCase2(){
account.deposit(3000);
account.setMilliSecondBase(100);
new Thread(() -> account.withdraw(1000)).start();
new Thread(() -> account.withdraw(1000)).start();
new Thread(() -> account.withdraw(1000)).start();
new Thread(() -> account.withdraw(1000)).start();
new Thread(() -> account.withdraw(1000)).start();
try {
Thread.sleep((int)(1000*2));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@DisplayName("Test Synchronization: Non-Static Method, 1000 ms (Hard See)")
@RepeatedTest( value = 10, name="{displayName} 第{currentRepetition}次測試,總共需要執行{totalRepetitions}次 ")
//@Test
public void testCase3(){
account.deposit(5000);
account.setMilliSecondBase(1000);
new Thread(() -> account.withdraw(1000)).start();
new Thread(() -> account.withdraw(1000)).start();
new Thread(() -> account.withdraw(1000)).start();
new Thread(() -> account.withdraw(1000)).start();
new Thread(() -> account.withdraw(1000)).start();
try {
Thread.sleep((int)(1000*10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@BeforeAll
public static void BeforeAll(){
System.out.println("Test Start");
}
@AfterAll
public static void AfterAll(){
System.out.println("Test End");
}
}
```
### 【範例結果】
**正常結果:**
:::success
Now have $ : 3000 $
Withdraw $ : 1000 $
Withdraw $ : 1000 $
Withdraw $ : 1000 $
Not enough money now $ : 0 $
Not enough money now $ : 0 $
:::
**不正常結果 1:**
:::danger
Now have $ : 3000 $
Withdraw $ : 1000 $
Withdraw $ : 1000 $
Withdraw $ : 1000 $
Withdraw $ : 1000 $ (多提領成功一次)
Not enough money now $ : 0 $
:::
**不正常結果 2 (非常少出現):**
:::danger
Now have $ : 3000 $
Withdraw $ : 1000 $
Withdraw $ : 1000 $
Withdraw $ : 1000 $
Withdraw $ : 1000 $ (多提領成功一次)
Withdraw $ : 1000 $ (多提領成功兩次)
:::
## 【Synchronized 介紹】
Synchronized 是 Java 中提供用來控制請求操作權限的修飾字,幫助開發者區別同步/非同步的東西。
Synchronized 在 Java 中的使用方式有以下幾種:
1. 方法控制
```java=
// 一般方法
public void targetMethod(){
//code
}
// 同步方法
public synchronized void targetMethod() {
//code
}
// 同步方法,不同寫法
synchronized public void targetMethod() {
//code
}
// 同步 static 方法
synchronized public static void targetMethod() {
//code
}
```
2. 方法內區塊控制
```java=
// 一般方法
public void targetMethod(){
//code
}
// 同步方法
public void targetMethod() {
synchronized(this){
//code
}
}
```
3. 物件控制
```java=
// 一般方法
public void targetMethod(){
Object targetObject;
//code
}
// 同步方法
public void targetMethod() {
Object targetObject;
synchronized(targetObject){
//code
}
}
```
特別要記住的點是,當 **Non-Static Method** or **Non-Static Field** 被修飾為 **Sychronized** 時,必須確保建立出的 **Instance** 只有一個,否則 **Synchronized** 修飾字將無法提供正確的保護。多數針對此種需求會採用 **Singleton** 設計方式,讓整個專案的 RunTime 只建立唯一一個 Instance。
而由於 **Static** 在 Run 的初期就會建立並且保持僅有一個 Instance 因此在使用 Synchronized 上就不需考量 Instance 的處理。
### 【Synchronized 範例】
**account.java**
```java=
package Thread.Synchronized;
public class account {
private int money;
private int milliSecondBase = 10;
public synchronized void withdraw(int withdrawMoney) {
try {
int totalTime = (int)(milliSecondBase*Math.random());
//System.out.println("This move need : " + totalTime + " secs to finish.");
Thread.sleep(totalTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
if((money - withdrawMoney) < 0) {
System.out.println("Not enough money now $ : " + money + " $");
return;
}
money -= withdrawMoney;
System.out.println("Withdraw $ : " + withdrawMoney + " $");
}
public void deposit(int depositMoney){
money += depositMoney;
System.out.println("Now have $ : " + money + " $");
}
public int getMoney(){
return this.money;
}
public void setMilliSecondBase(int newMilliSecondBase){
this.milliSecondBase = newMilliSecondBase;
}
}
```
**test.java**
```java=
package Thread.Synchronized;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;
public class test {
private account account = new account();
@DisplayName("Test Synchronization: Non-Static Method, 10 ms (Usually See)")
@RepeatedTest( value = 10, name="{displayName} 第{currentRepetition}次測試,總共需要執行{totalRepetitions}次 ")
public void testCase1(){
account.deposit(3000);
account.setMilliSecondBase(10);
new Thread(() -> account.withdraw(1000)).start();
new Thread(() -> account.withdraw(1000)).start();
new Thread(() -> account.withdraw(1000)).start();
new Thread(() -> account.withdraw(1000)).start();
new Thread(() -> account.withdraw(1000)).start();
try {
Thread.sleep((int)(1000*2));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@DisplayName("Test Synchronization: Non-Static Method, 100 ms (Medium See)")
@RepeatedTest( value = 10, name="{displayName} 第{currentRepetition}次測試,總共需要執行{totalRepetitions}次 ")
public void testCase2(){
account.deposit(3000);
account.setMilliSecondBase(100);
new Thread(() -> account.withdraw(1000)).start();
new Thread(() -> account.withdraw(1000)).start();
new Thread(() -> account.withdraw(1000)).start();
new Thread(() -> account.withdraw(1000)).start();
new Thread(() -> account.withdraw(1000)).start();
try {
Thread.sleep((int)(1000*2));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@DisplayName("Test Synchronization: Non-Static Method, 1000 ms (Hard See)")
@RepeatedTest( value = 10, name="{displayName} 第{currentRepetition}次測試,總共需要執行{totalRepetitions}次 ")
public void testCase3(){
account.deposit(3000);
account.setMilliSecondBase(1000);
new Thread(() -> account.withdraw(1000)).start();
new Thread(() -> account.withdraw(1000)).start();
new Thread(() -> account.withdraw(1000)).start();
new Thread(() -> account.withdraw(1000)).start();
new Thread(() -> account.withdraw(1000)).start();
try {
Thread.sleep((int)(1000*10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@BeforeAll
public static void BeforeAll(){
System.out.println("Test Start");
}
@AfterAll
public static void AfterAll(){
System.out.println("Test End");
}
}
```
### 【範例結果】
由於設置了 synchronized 修飾字,因此方法在同個時間內只會被一個請求使用,因此不會出現一種以外的結果。
:::success
Now have $ : 3000 $
Withdraw $ : 1000 $
Withdraw $ : 1000 $
Withdraw $ : 1000 $
Not enough money now $ : 0 $
Not enough money now $ : 0 $
:::
:::warning
由於 Kai 在 Non-Synchronized 的部分有其他的程式,因此造成同步/非同步的範例程式命名不同,實際上其結果的代表意義不受命名影響。
:::
首頁 [Kai 個人技術 Hackmd](/2G-RoB0QTrKzkftH2uLueA)
###### tags: `Spring Boot`