Java并發編程-理論基礎
1、什么是進程?
進程(Process)是計算機中的程序關于某數據集合上的一次運行活動,是系統進行資源分配的基本單位,是操作系統結構的基礎。在早期面向進程設計的計算機結構中,進程是程序的基本執行實體;在當代面向線程設計的計算機結構中,進程是線程的容器。程序是指令、數據及其組織形式的描述,進程是程序的實體。
2、什么是線程?
線程(英語:thread)是操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以并發多個線程,每條線程并行執行不同的任務。在Unix System V及SunOS中也被稱為輕量進程(lightweight processes),但輕量進程更多指內核線程(kernel thread),而把用戶線程(user thread)稱為線程。
線程是獨立調度和分派的基本單位。線程可以為操作系統內核調度的內核線程,如Win32線程;由用戶進程自行調度的用戶線程,如Linux平臺的POSIX Thread;或者由內核與用戶進程,如Windows 7的線程,進行混合調度。
同一進程中的多條線程將共享該進程中的全部系統資源,如虛擬地址空間,文件描述符和信號處理等等。但同一進程中的多個線程有各自的調用棧(call stack),自己的寄存器環境(register context),自己的線程本地存儲(thread-local storage)。
一個進程可以有很多線程,每條線程并行執行不同的任務。
在多核或多CPU,或支持Hyper-threading的CPU上使用多線程程序設計的好處是顯而易見,即提高了程序的執行吞吐率。在單CPU單核的計算機上,使用多線程技術,也可以把進程中負責I/O處理、人機交互而常被阻塞的部分與密集計算的部分分開來執行,編寫專門的workhorse線程執行密集計算,從而提高了程序的執行效率。
進程與線程的對比:
特性 | 進程 | 線程 |
定義 | 資源分配的基本單位 | CPU調度的基本單位 |
內存空間 | 獨立內存空間 | 共享進程內存空間 |
通信 | 需要IPC機制(管道、消息隊列等) | 可直接讀寫進程數據段 |
創建開銷 | 大(需要分配獨立資源) | 小(共享已有資源) |
穩定性 | 一個進程崩潰不影響其他進程 | 一個線程崩潰可能導致整個進程崩潰 |
并發性 | 進程間并發 | 線程間并發 |
3、為什么需要多線程?
從CPU、內存、I/O速度差異的角度分析
計算機系統中,CPU、內存、I/O設備的速度差異極大:
- CPU 計算速度極快(納秒級)
- 內存 訪問速度較慢(百納秒級)
- I/O設備(磁盤、網絡)更慢(毫秒級,比CPU慢百萬倍)
為了合理利用CPU的高性能,計算機體系結構、操作系統和編譯器分別采用了優化手段,但也引入了并發問題:
1. CPU緩存(Cache)——解決CPU與內存速度差異
- 問題:CPU計算速度遠高于內存訪問速度,直接讀寫內存會導致CPU大部分時間等待(內存墻問題)。
- 解決方案:引入多級緩存(L1/L2/L3 Cache),減少CPU訪問內存的次數。
- 副作用(可見性問題):
-
- 多線程環境下,一個線程修改了緩存數據,另一個線程可能無法立即看到(緩存一致性問題)。
- 需要內存屏障(Memory Barrier)或volatile關鍵字保證可見性。
總結:緩存提高CPU利用率,但導致多線程間數據不可見。
2. 進程/線程分時復用——解決CPU與I/O速度差異
- 問題:I/O操作(磁盤、網絡)極慢,單線程程序會讓CPU大部分時間等待I/O。
- 解決方案:
-
- 進程:操作系統提供隔離的執行環境,但切換開銷大。
- 線程:輕量級執行單元,共享進程資源,切換成本低。
- 多線程:當一個線程等待I/O時,CPU可以執行其他線程(提高利用率)。
- 副作用(原子性問題):
-
- 線程切換可能導致非原子操作被中斷(如
i++
并非原子操作)。 - 需要鎖(synchronized)或CAS(Compare-And-Swap)保證原子性。
- 線程切換可能導致非原子操作被中斷(如
總結:多線程提高CPU利用率,但導致競態條件(Race Condition)。
3. 編譯器/CPU指令重排序——優化緩存利用率
- 問題:為了減少CPU等待數據的時間,編譯器和CPU會優化指令執行順序(如亂序執行)。
- 解決方案:
-
- 指令重排:調整無關指令的順序,提高緩存命中率。
- 副作用(有序性問題):
-
- 多線程環境下,指令重排可能導致邏輯錯誤(如單例模式的雙重檢查鎖失效)。
- 需要內存屏障(Memory Barrier)或
volatile
禁止重排序。
總結:指令重排提高性能,但導致多線程執行順序不可預測。
4、線程不安全如何產生的?(Java中)
如果多個線程對同一個共享數據進行訪問而不采取同步操作的話,那么操作的結果是不一致的。
銀行賬戶取款問題
public class UnsafeBankAccount {private int balance; // 共享數據public UnsafeBankAccount(int initialBalance) {this.balance = initialBalance;}// 不安全的取款方法public void withdraw(int amount) {if (amount <= balance) {// 模擬一些處理時間try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}balance -= amount;System.out.println(Thread.currentThread().getName() + " 取款 " + amount + " 成功,余額: " + balance);} else {System.out.println(Thread.currentThread().getName() + " 取款 " + amount + " 失敗,余額不足");}}public static void main(String[] args) {UnsafeBankAccount account = new UnsafeBankAccount(1000);// 創建多個線程同時取款for (int i = 0; i < 10; i++) {new Thread(() -> account.withdraw(800)).start();}}
}
運行結果:
問題分析
- 競態條件(Race Condition)
-
- 多個線程同時檢查余額
if (amount <= balance)
- 在檢查后但實際扣款前,其他線程可能已經修改了余額
- 導致多個線程都認為可以取款,最終余額變為負數
- 多個線程同時檢查余額
- 操作非原子性
balance -= amount
不是原子操作,它實際上是:
-
- java復制下載
int temp = balance;
temp = temp - amount;
balance = temp;
-
- 線程可能在中間步驟被中斷
- 內存可見性問題
-
- 一個線程對balance的修改可能不會立即對其他線程可見
- 導致線程看到的是過期的balance值
線程不安全的核心原因
- 共享數據的并發訪問:多個線程同時讀寫同一個變量
- 操作的非原子性:操作不是一次性完成的,可能被中斷
- 缺乏同步機制:沒有使用synchronized、Lock等同步手段
- 內存可見性問題:線程可能看不到其他線程的最新修改
5、并發三要素
在并發編程中,問題的根源可以歸結為三個核心要素:可見性、原子性、有序性
可見性(visibility)
可見性問題指的是一個線程對共享變量的修改,另一個線程不能立即看到。這是由于現代計算機架構中CPU緩存與主內存之間的同步延遲導致的。
可見性問題源于現代計算機的多級存儲架構:
- CPU緩存架構:每個CPU核心有自己的緩存(L1、L2),多個核心共享L3緩存
- 緩存一致性協議:如MESI協議,但存在延遲
- 編譯器優化:可能將變量緩存在寄存器中而不是從內存讀取
在Java內存模型(JMM)中,每個線程有自己的工作內存(可以理解為CPU緩存的抽象),導致線程對共享變量的修改可能不會立即同步到主內存,其他線程也就無法立即看到修改。
可見性完整案例
案例1:體現可見性問題的服務控制器(基礎可見性問題)
package com.taiyuan.javademoone.threaddemo.demo001;import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** 修改后的ServiceController,強制體現可見性問題* 通過純空循環和長時間運行來暴露可見性問題*/
public class ServiceController {// 無volatile修飾,保證會出現可見性問題private static boolean isRunning = true;//TODO 解決方案1、添加volatile修飾,保證可見性// private static volatile boolean isRunning = true;// TODO 解決方案2、AtomicBoolean,保證可見性// private static AtomicBoolean isRunning = new AtomicBoolean(true);// TODO 解決方案3、使用synchronized關鍵字,保證可見性// public static synchronized boolean isRunning() {// return isRunning;// }// public static synchronized void stopRunning() {// isRunning = false;// }// TODO 解決方案4、使用Lock,保證可見性private static final Lock lock = new ReentrantLock();public static boolean isRunning() {lock.lock();try {return isRunning;} finally {lock.unlock();}}public static void stopRunning() {lock.lock();try {isRunning = false;} finally {lock.unlock();}}// 添加一個計數器,用于驗證循環執行次數private static long counter = 0;public static void main(String[] args) throws InterruptedException {// 工作線程 - 使用純空循環Thread worker = new Thread(() -> {while (isRunning) {counter++; // 純計數操作,無任何同步點}System.out.println("工作線程停止,循環次數: " + counter);});worker.start();// 主線程稍后停止服務Thread.sleep(3000);isRunning = false;// stopRunning();System.out.println("主線程將isRunning設置為false");// 等待工作線程結束(最多10秒)worker.join(10000);if (worker.isAlive()) {System.out.println("警告:工作線程未能正常停止!");System.out.println("最后計數: " + counter);}}
}
解決可見性問題:
方案1:使用volatile(推薦)
private static volatile boolean isRunning = true;
方案2:使用AtomicBoolean
private static AtomicBoolean isRunning = new AtomicBoolean(true);// 在循環中
while (isRunning.get()) { ... }// 設置停止
isRunning.set(false);
方案3:使用synchronized方法
private static boolean isRunning = true;public static synchronized boolean isRunning() {return isRunning;
}public static synchronized void stopRunning() {isRunning = false;
}// 使用方式
while (isRunning()) { ... }
stopRunning();
方案4:使用Lock
private static boolean isRunning = true;
private static final Lock lock = new ReentrantLock();public static boolean isRunning() {lock.lock();try {return isRunning;} finally {lock.unlock();}
}public static void stopRunning() {lock.lock();try {isRunning = false;} finally {lock.unlock();}
}
原子性(Atomicity)
原子性是指一個操作或一系列操作作為一個不可分割的整體執行,要么全部完成,要么完全不執行。在多線程環境中,非原子操作會導致競態條件(Race Condition),產生不可預期的結果。
原子性三要素:
- 不可分割:操作過程不會被線程調度打斷
- 完全成功或完全失敗:沒有中間狀態
- 狀態一致性:操作前后系統狀態保持一致
原子性案例:
1、銀行賬戶轉賬(經典競態條件)
package com.taiyuan.javademoone.threaddemo.demo002;/*** BankAccount 類表示一個銀行賬戶,用于演示多線程環境下的轉賬操作*/
public class BankAccount {// 定義賬戶余額private int balance;// 構造函數,初始化賬戶余額public BankAccount(int initialBalance) {this.balance = initialBalance;}/** transfer 方法用于從一個賬戶向另一個賬戶轉賬*/public void transfer(BankAccount dest, int amount) {// 判斷賬戶余額是否足夠if (this.balance >= amount) {// 模擬處理延遲(放大競態窗口)try { Thread.sleep(10); } catch (InterruptedException e) {}// 扣款this.balance -= amount;// 收款dest.balance += amount;// 打印轉賬成功信息System.out.println(Thread.currentThread().getName() +" 轉賬成功: " + amount);}}/*** 獲取賬戶余額*/public int getBalance() {return balance;}
}
2、測試類(暴露問題)
package com.taiyuan.javademoone.threaddemo.demo002;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;/*** 銀行轉賬示例*/
public class BankTransferDemo {public static void main(String[] args) throws InterruptedException {// 創建兩個賬戶,初始余額為1000BankAccount accountA = new BankAccount(1000);BankAccount accountB = new BankAccount(1000);// 輸出初始余額System.out.println("初始余額:");System.out.println("賬戶A: " + accountA.getBalance());System.out.println("賬戶B: " + accountB.getBalance());// 創建轉賬線程池ExecutorService executor = Executors.newFixedThreadPool(10);// 模擬100次從A向B轉賬10元for (int i = 0; i < 100; i++) {executor.execute(() -> accountA.transfer(accountB, 10));}// 模擬100次從B向A轉賬10元for (int i = 0; i < 100; i++) {executor.execute(() -> accountB.transfer(accountA, 10));}// 關閉線程池executor.shutdown();// 等待線程池中的任務執行完畢executor.awaitTermination(1, TimeUnit.MINUTES);// 輸出最終余額System.out.println("\n最終余額:");System.out.println("賬戶A: " + accountA.getBalance());System.out.println("賬戶B: " + accountB.getBalance());System.out.println("總額: " + (accountA.getBalance() + accountB.getBalance()));}
}
測試結果
結果分析:
出現總額不為2070的情況,說明發生了競態條件導致金額不一致。
3、解決方案
1、synchronized方法同步
package com.taiyuan.javademoone.threaddemo.demo003;/*** 線程安全的銀行賬戶類(使用synchronized解決原子性問題)*/
public class SynchronizedBankAccount {private int balance;public SynchronizedBankAccount(int initialBalance) {this.balance = initialBalance;}/*** 線程安全的轉賬方法** @param dest 目標賬戶* @param amount 轉賬金額* @return 轉賬成功返回true,否則返回false*/public boolean transfer(SynchronizedBankAccount dest, int amount) {// 按照賬戶對象的哈希值確定鎖順序,避免死鎖SynchronizedBankAccount first = this.hashCode() < dest.hashCode() ? this : dest;SynchronizedBankAccount second = this.hashCode() < dest.hashCode() ? dest : this;// 先鎖定賬戶1,再鎖定賬戶2 (避免死鎖)synchronized (first) {synchronized (second) {// 檢查余額是否充足if (this.balance < amount) {System.out.println(Thread.currentThread().getName() +" 轉賬失敗: 余額不足 (當前余額=" + this.balance + ")");return false;}// 模擬處理延遲try {Thread.sleep(10);} catch (InterruptedException e) {Thread.currentThread().interrupt();return false;}// 執行轉賬this.balance -= amount;dest.balance += amount;System.out.println(Thread.currentThread().getName() +" 轉賬成功: " + amount +" (轉出賬戶余額=" + this.balance +", 目標賬戶余額=" + dest.balance + ")");return true;}}}/*** 獲取賬戶余額(線程安全)*/public synchronized int getBalance() {return balance;}
}
測試:
package com.taiyuan.javademoone.threaddemo.demo003;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;/*** 銀行賬戶轉賬測試類* 本類用于測試在多線程環境下,兩個銀行賬戶之間的轉賬操作是否能夠正確執行* 通過創建兩個初始余額相同的銀行賬戶,使用固定數量的線程執行多次相互轉賬操作,* 最后檢查兩個賬戶的總余額是否與初始總余額相等,以此驗證轉賬操作的線程安全性*/
public class BankTransferTest {// 初始余額常量private static final int INITIAL_BALANCE = 1000;// 每次轉賬金額常量private static final int TRANSFER_AMOUNT = 10;// 轉賬操作次數常量private static final int TRANSFER_COUNT = 100;// 線程池線程數量常量private static final int THREAD_COUNT = 10;public static void main(String[] args) throws InterruptedException {// 創建兩個銀行賬戶SynchronizedBankAccount accountA = new SynchronizedBankAccount(INITIAL_BALANCE);SynchronizedBankAccount accountB = new SynchronizedBankAccount(INITIAL_BALANCE);// 打印初始余額System.out.println("========== 初始余額 ==========");System.out.println("賬戶A余額: " + accountA.getBalance());System.out.println("賬戶B余額: " + accountB.getBalance());System.out.println("總額: " + (accountA.getBalance() + accountB.getBalance()));// 創建線程池ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);// 啟動轉賬任務System.out.println("\n========== 開始轉賬 ==========");for (int i = 0; i < TRANSFER_COUNT; i++) {// A向B轉賬executor.execute(() -> accountA.transfer(accountB, TRANSFER_AMOUNT));// B向A轉賬executor.execute(() -> accountB.transfer(accountA, TRANSFER_AMOUNT));}// 關閉線程池并等待任務完成executor.shutdown();executor.awaitTermination(1, TimeUnit.MINUTES);// 打印最終余額System.out.println("\n========== 最終余額 ==========");System.out.println("賬戶A余額: " + accountA.getBalance());System.out.println("賬戶B余額: " + accountB.getBalance());// 驗證總額不變int total = accountA.getBalance() + accountB.getBalance();int expectedTotal = 2 * INITIAL_BALANCE;System.out.println("總額: 實際=" + total + ", 預期=" + expectedTotal +(total == expectedTotal ? " (正確)" : " (錯誤!)"));}
}
運行結果:
2、ReentrantLock顯式鎖
package com.taiyuan.javademoone.threaddemo.demo004;import java.util.concurrent.locks.ReentrantLock;/*** BankAccount 類表示一個銀行賬戶,使用ReentrantLock保證線程安全*/
public class BankLockAccount {// 賬戶余額private int balance;// 可重入鎖private final ReentrantLock lock = new ReentrantLock();// 構造函數,初始化賬戶余額public BankLockAccount(int initialBalance) {this.balance = initialBalance;}/*** transfer 方法用于從一個賬戶向另一個賬戶轉賬* 使用ReentrantLock保證線程安全*/public void transfer(BankLockAccount dest, int amount) {// 確定鎖獲取順序(例如通過hashCode比較)這種通過對象哈希碼決定鎖順序的方法是一種常見的死鎖避免策略,稱為鎖排序(Lock Ordering)。BankLockAccount first = this.hashCode() < dest.hashCode() ? this : dest;BankLockAccount second = this.hashCode() < dest.hashCode() ? dest : this;first.lock.lock();try {second.lock.lock();try {if (this.balance >= amount) {try { Thread.sleep(10); } catch (InterruptedException e) {}this.balance -= amount;dest.balance += amount;System.out.println(Thread.currentThread().getName() +" 轉賬成功: " + amount);}} finally {second.lock.unlock();}} finally {first.lock.unlock();}}/*** 獲取賬戶余額*/public int getBalance() {lock.lock();try {return balance;} finally {lock.unlock();}}
}
測試:
package com.taiyuan.javademoone.threaddemo.demo004;import java.util.concurrent.locks.ReentrantLock;/*** BankAccount 類表示一個銀行賬戶,使用ReentrantLock保證線程安全*/
public class BankLockAccount {// 賬戶余額private int balance;// 可重入鎖private final ReentrantLock lock = new ReentrantLock();// 構造函數,初始化賬戶余額public BankLockAccount(int initialBalance) {this.balance = initialBalance;}/*** transfer 方法用于從一個賬戶向另一個賬戶轉賬* 使用ReentrantLock保證線程安全*/public void transfer(BankLockAccount dest, int amount) {// 確定鎖獲取順序(例如通過hashCode比較)這種通過對象哈希碼決定鎖順序的方法是一種常見的死鎖避免策略,稱為鎖排序(Lock Ordering)。BankLockAccount first = this.hashCode() < dest.hashCode() ? this : dest;BankLockAccount second = this.hashCode() < dest.hashCode() ? dest : this;first.lock.lock();try {second.lock.lock();try {if (this.balance >= amount) {try { Thread.sleep(10); } catch (InterruptedException e) {}this.balance -= amount;dest.balance += amount;System.out.println(Thread.currentThread().getName() +" 轉賬成功: " + amount);}} finally {second.lock.unlock();}} finally {first.lock.unlock();}}/*** 獲取賬戶余額*/public int getBalance() {lock.lock();try {return balance;} finally {lock.unlock();}}
}
3、使用AtomicReference(無鎖方案)
package com.taiyuan.javademoone.threaddemo.demo0005;import java.util.concurrent.atomic.AtomicReference;/*** 使用AtomicReference實現的無鎖銀行賬戶*/
public class AtomicBankAccount {// 使用AtomicReference包裝賬戶余額// AtomicReference<Integer>:提供對 Integer 類型值的原子操作,確保多線程環境下數據一致性。/*** AtomicReference 是 Java 中 java.util.concurrent.atomic 包的一部分,* 它提供了一種原子操作方式,用于更新引用類型的變量。* 與其他原子類(如 AtomicInteger 和 AtomicLong)類似,* AtomicReference 允許你在多線程環境下安全地對對象引用進行更新,* 而無需使用傳統的同步機制(例如 synchronized 關鍵字)。*/private final AtomicReference<Integer> balanceRef;public AtomicBankAccount(int initialBalance) {this.balanceRef = new AtomicReference<>(initialBalance);}/*** 無鎖轉賬實現** @param dest 目標賬戶* @param amount 轉賬金額* @return 轉賬成功返回true,否則返回false*/public boolean transfer(AtomicBankAccount dest, int amount) {// 參數檢查if (amount <= 0) {return false;}/** CAS 是一種 硬件支持的原子指令,它在 Java 中通過 AtomicReference 和 Unsafe 類等機制實現。它的核心思想是:* 在更新一個值時,先檢查該值是否與預期一致,如果一致則更新為新值,否則重試。*/// 使用CAS循環實現原子轉賬操作while (true) {// 獲取當前轉出賬戶余額的快照Integer current = balanceRef.get();// 檢查當前余額是否足夠進行轉賬if (current < amount) {// 余額不足時打印轉賬失敗信息,并返回false表示轉賬失敗System.out.println(Thread.currentThread().getName() +" 轉賬失敗: 余額不足");return false;}// 模擬處理轉賬過程中的延遲,增加并發情況下沖突的可能性try {Thread.sleep((int) (Math.random() * 10));} catch (InterruptedException e) {// 當線程被中斷時,設置中斷標志并返回false表示轉賬失敗Thread.currentThread().interrupt();return false;}// 使用CAS操作更新轉出賬戶的余額if (balanceRef.compareAndSet(current, current - amount)) {// 更新目標賬戶余額,同樣需要使用CAS操作以確保原子性while (true) {// 獲取目標賬戶當前余額的快照Integer destCurrent = dest.balanceRef.get();// 使用CAS操作更新目標賬戶余額,成功則打印轉賬信息并返回true表示轉賬成功if (dest.balanceRef.compareAndSet(destCurrent, destCurrent + amount)) {System.out.println(Thread.currentThread().getName() +" 轉賬成功: " + amount +" (轉出賬戶余額=" + (current - amount) +", 目標賬戶余額=" + (destCurrent + amount) + ")");return true;}}}// CAS操作失敗則重試整個轉賬過程}}public int getBalance() {return balanceRef.get();}
}
測試:
package com.taiyuan.javademoone.threaddemo.demo0005;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;/*** AtomicBankTransferTest 類用于演示使用原子操作實現銀行賬戶轉賬的線程安全性*/
public class AtomicBankTransferTest {// 定義初始余額常量private static final int INITIAL_BALANCE = 1000;// 定義每次轉賬金額常量private static final int TRANSFER_AMOUNT = 10;// 定義轉賬次數常量private static final int TRANSFER_COUNT = 100;// 定義線程池大小常量private static final int THREAD_COUNT = 10;/*** 主函數執行多個線程安全的轉賬操作* @param args 命令行參數* @throws InterruptedException 如果在等待線程池終止時被中斷*/public static void main(String[] args) throws InterruptedException {// 初始化兩個賬戶,賬戶A和賬戶B,每個賬戶初始余額為INITIAL_BALANCEAtomicBankAccount accountA = new AtomicBankAccount(INITIAL_BALANCE);AtomicBankAccount accountB = new AtomicBankAccount(INITIAL_BALANCE);// 打印初始余額System.out.println("初始余額:");System.out.println("賬戶A: " + accountA.getBalance());System.out.println("賬戶B: " + accountB.getBalance());// 創建固定大小的線程池ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);// 啟動轉賬任務for (int i = 0; i < TRANSFER_COUNT; i++) {// 從賬戶A向賬戶B轉賬TRANSFER_AMOUNT金額executor.execute(() -> accountA.transfer(accountB, TRANSFER_AMOUNT));// 從賬戶B向賬戶A轉賬TRANSFER_AMOUNT金額executor.execute(() -> accountB.transfer(accountA, TRANSFER_AMOUNT));}// 關閉線程池,并等待所有任務完成executor.shutdown();// 等待所有任務完成executor.awaitTermination(1, TimeUnit.MINUTES);// 打印最終余額System.out.println("\n最終余額:");System.out.println("賬戶A: " + accountA.getBalance());System.out.println("賬戶B: " + accountB.getBalance());System.out.println("總額: " + (accountA.getBalance() + accountB.getBalance()));}
}
有序性(Ordering)
有序性(ordering):指程序執行的順序必須符合預期,不能出現亂序的情況。在多線程環境下,由于編譯器、處理器、緩存等因素的影響,程序執行的順序可能會出現不一致的情況,導致程序出現錯誤。為了保證有序性,可以使用volatile關鍵字或者顯式地使用鎖來實現。同時,Java提供了happens-before規則,它可以保證在特定情況下,操作的順序是按照預期的順序執行的。
導致有序性問題的三大原因:
- 編譯器優化重排序:在不改變單線程語義的前提下,編譯器可能調整指令順序
- 處理器指令級并行:現代CPU采用流水線、亂序執行等技術
- 內存系統重排序:由于多級緩存的存在,內存操作可能表現出亂序行為
Java提供了多種機制來保證有序性:
1. volatile關鍵字
volatile關鍵字可以:
- 禁止指令重排序(通過內存屏障實現)
- 保證變量的可見性
2. synchronized關鍵字
synchronized通過互斥鎖保證:
- 同一時刻只有一個線程能訪問同步代碼塊
- 在進入同步塊前會清空工作內存,退出時會將變量刷新回主內存
- 禁止指令重排序
3. final關鍵字
正確初始化的final字段可以保證:
- 在構造函數完成初始化后對其他線程可見
- 禁止對final字段的寫操作重排序到構造函數之外
4. happens-before原則
Java內存模型定義的happens-before關系保證了有序性,包括:
- 程序順序規則
- 監視器鎖規則
- volatile變量規則
- 線程啟動規則
- 線程終止規則
- 線程中斷規則
- 對象終結規則
- 傳遞性
5. 原子類
java.util.concurrent.atomic包下的原子類使用CAS操作和volatile語義保證有序性。
6、什么是happens-before原則?
Happens-Before原則是Java內存模型(JMM)的核心理論,它從語言層面定義了多線程環境中操作之間的可見性規則,為開發者提供了一種理解并發程序行為的框架。
Happens-Before關系本質上是一種可見性保證契約:
- 如果操作A happens-before 操作B,那么A的所有寫操作對B都是可見的
- 這種關系可以跨線程傳遞
- JVM必須遵守這些規則,但可以在不違反規則的前提下進行優化
Happens-Before(先行發生)原則的定義:
- 程序次序規則(Program Order Rule):在一個線程內,按照控制流順序,書寫在前面的操作先行發生于書寫在后面的操作。
- 管程鎖定規則(Monitor Lock Rule):一個unlock操作先行發生于后面對同一個鎖的lock操作。
- volatile變量規則(Volatile Variable Rule):對一個volatile變量的寫操作先行發生于后面對這個變量的讀操作。
- 線程啟動規則(Thread Start Rule):Thread對象start()方法先行發生于此線程的每一個動作。
- 線程終止規則(Thread Termination Rule):線程中的所有操作都先行發生于對此線程的終止檢測,我們可以通過Thread.join()方法和Thread.isAlive()的返回值等手段檢測線程是否已經終止執行。
- 線程中斷規則(Thread Interruption Rule):對線程interrupt()方法的調用先行發生于被中斷線程的代碼檢測到中斷事件的發生,可以通過Thread.interrupted()方法檢測到是否有中斷發生。
- 對象終結規則(Finalizer Rule) :一個對象的初始化完成(構造函數結束)先行發生于它的finalize()方法的開始。
- 傳遞性(Transitivity):如果操作A先行發生于操作B,操作B先行發生于操作C,那就可以得出操作A先行發生于操作C的結論。
7、volatile、synchronized 和 final詳解
1. volatile關鍵字
特性:
- 可見性:保證變量的修改對所有線程立即可見
- 有序性:禁止指令重排序(通過內存屏障實現)
- 不保證原子性:復合操作仍需同步
適用場景:
- 單寫多讀的場景
- 作為狀態標志位
- 雙重檢查鎖定模式(DCL)
2. synchronized關鍵字
特性:
- 互斥性:同一時刻只有一個線程能進入同步塊
- 可見性:同步塊內的變量修改對其他線程可見
- 原子性:保證代碼塊內的操作不可分割
- 有序性:禁止指令重排序
使用方式:
- 同步實例方法
- 同步靜態方法
- 同步代碼塊
3、final關鍵字
特性:
- 不可變性:基本類型值不可變,引用類型引用不可變
- 線程安全:正確構造的對象對所有線程可見
- 初始化安全:禁止final字段的寫操作重排序到構造函數之外
使用場景:
- 常量定義
- 不可變對象
- 安全發布對象