# 牛年自強計畫 Week 3 - SpringBoot - 番外篇 Java 執行緒 Synchronized/Non-Synchronized ## 【前言】 了解 **Thread** 和 **ThreadPool** 後,接著就來看使用 Thread 後會遇到的**同步/非同步問題**。 我們先看一個基本的銀行提款例子! ![](https://i.imgur.com/FW4amhs.png) - 當在銀行有總額 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`