深入理解Java線程安全與鎖優化
一、概述:從現實世界到計算機世界
在軟件開發的早期,程序員采用面向過程的編程思想,將數據和操作分離。而面向對象編程則更符合現實世界的思維方式,把數據和行為都封裝在對象中。然而,現實世界與計算機世界之間存在一個重要差異:在計算機世界中,對象的工作可能會被頻繁中斷和切換,屬性可能在中斷期間被修改,這導致了線程安全問題的產生。
// 一個簡單的計數器類
public class Counter {
private int count = 0;
public void increment() {
count++; // 非原子操作,存在線程安全問題
}
public int getCount() {
return count;
}
}
當我們開始討論"高效并發"時,首先需要確保并發的正確性,然后才考慮如何實現高效。這正是本章要探討的核心內容。
二、線程安全的定義與分類
2.1 什么是線程安全?
Brian Goetz在《Java并發編程實戰》中給出了一個精準的定義:
"當多個線程同時訪問一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替執行,也不需要進行額外的同步,或者在調用方進行任何其他的協調操作,調用這個對象的行為都可以獲得正確的結果,那就稱這個對象是線程安全的。"
這個定義要求線程安全的代碼必須封裝所有必要的正確性保障手段,使調用者無需關心多線程問題。
2.2 Java語言中的線程安全等級
我們可以按照線程安全的"安全程度"將Java中的共享數據操作分為五類:
1. 不可變(Immutable)
不可變對象一定是線程安全的,因為它們的可見狀態永遠不會改變。
// 使用final關鍵字創建不可變對象
public final class ImmutableValue {
private final int value;
public ImmutableValue(int value) {
this.value = value;
}
public int getValue() {
return value;
}
// 返回新對象而不是修改現有對象
public ImmutableValue add(int delta) {
return new ImmutableValue(this.value + delta);
}
}
Java中的String、Integer、Long等包裝類都是不可變的。
2. 絕對線程安全
絕對線程安全完全滿足Brian Goetz的定義,但實踐中很難實現。即使Java中標注為線程安全的類,如Vector,也并非絕對線程安全。
// Vector的線程安全局限性示例
public class VectorTest {
private static Vector<Integer> vector = new Vector<>();
public static void main(String[] args) {
while (true) {
for (int i = 0; i < 10; i++) {
vector.add(i);
}
Thread removeThread = new Thread(() -> {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
});
Thread printThread = new Thread(() -> {
for (int i = 0; i < vector.size(); i++) {
System.out.println(vector.get(i));
}
});
removeThread.start();
printThread.start();
// 不要同時產生過多線程,防止操作系統假死
while (Thread.activeCount() > 20) ;
}
}
}
上述代碼可能拋出ArrayIndexOutOfBoundsException,因為雖然Vector的每個方法都是同步的,但復合操作(先檢查再執行)仍需外部同步。
3. 相對線程安全
相對線程安全保證單次操作是線程安全的,但特定順序的連續調用可能需要外部同步。Java中大部分聲稱線程安全的類屬于此類,如Vector、HashTable等。
4. 線程兼容
線程兼容指對象本身不是線程安全的,但可以通過正確使用同步手段保證安全。如ArrayList、HashMap等。
5. 線程對立
線程對立指無論是否采取同步措施,都無法在多線程環境中安全使用。如Thread類的suspend()和resume()方法。
三、線程安全的實現方法
3.1 互斥同步
互斥同步是最常見的并發保障手段,synchronized是最基本的互斥同步手段。
synchronized的實現原理
public class SynchronizedExample {
// 同步實例方法
public synchronized void instanceMethod() {
// 同步代碼
}
// 同步靜態方法
public static synchronized void staticMethod() {
// 同步代碼
}
public void method() {
// 同步塊
synchronized(this) {
// 同步代碼
}
}
}
synchronized編譯后會在同步塊前后生成monitorenter和monitorexit字節碼指令。執行monitorenter時:
- 如果對象未被鎖定,或當前線程已持有鎖,則鎖計數器+1
- 如果獲取鎖失敗,當前線程阻塞直到鎖被釋放
synchronized的特性:
- 可重入:同一線程可重復獲取同一把鎖
- 阻塞性:未獲取鎖的線程會無條件阻塞
- 重量級:線程阻塞和喚醒需要操作系統介入,成本高
ReentrantLock:更靈活的互斥同步
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void method() {
lock.lock(); // 獲取鎖
try {
// 同步代碼
} finally {
lock.unlock(); // 確保鎖被釋放
}
}
}
ReentrantLock相比synchronized的高級特性:
- 等待可中斷:避免長期等待
public boolean tryLockWithTimeout() throws InterruptedException {
return lock.tryLock(5, TimeUnit.SECONDS); // 最多等待5秒
}
- 公平鎖:按申請順序獲取鎖
private final ReentrantLock fairLock = new ReentrantLock(true); // 公平鎖
- 綁定多個條件
public class ConditionExample {
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public void await() throws InterruptedException {
lock.lock();
try {
condition.await(); // 釋放鎖并等待
} finally {
lock.unlock();
}
}
public void signal() {
lock.lock();
try {
condition.signal(); // 喚醒等待線程
} finally {
lock.unlock();
}
}
}
synchronized vs ReentrantLock
- 簡單性:synchronized更簡單清晰
- 性能:JDK6后兩者性能相近
- 功能:ReentrantLock更靈活
- 推薦:優先使用synchronized,需要高級功能時使用ReentrantLock
3.2 非阻塞同步
非阻塞同步基于沖突檢測的樂觀并發策略,先操作后檢測沖突。
CAS(Compare-and-Swap)原理
CAS操作需要三個參數:內存位置V、舊預期值A和新值B。當且僅當V的值等于A時,才用B更新V的值。
public class CASExample {
private AtomicInteger atomicValue = new AtomicInteger(0);
public void increment() {
int oldValue;
int newValue;
do {
oldValue = atomicValue.get(); // 獲取當前值
newValue = oldValue + 1; // 計算新值
} while (!atomicValue.compareAndSet(oldValue, newValue)); // CAS操作
}
}
Java中的原子類(如AtomicInteger)使用CAS實現無鎖線程安全:
public class AtomicExample {
public static AtomicInteger race = new AtomicInteger(0);
public static void increase() {
race.incrementAndGet(); // 原子自增
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[20];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
increase();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
System.out.println(race.get()); // 總是輸出200000
}
}
ABA問題
CAS操作存在ABA問題:如果一個值從A變成B,又變回A,CAS操作會誤以為它沒變化。
解決方案:使用AtomicStampedReference或AtomicMarkableReference維護版本號。
public class ABAExample {
public static void main(String[] args) {
AtomicStampedReference<Integer> atomicRef =
new AtomicStampedReference<>(100, 0);
int stamp = atomicRef.getStamp();
Integer reference = atomicRef.getReference();
// 更新值并增加版本號
atomicRef.compareAndSet(reference, 101, stamp, stamp + 1);
}
}
3.3 無同步方案
可重入代碼(純代碼)
可重入代碼不依賴共享數據,所有狀態都由參數傳入,不會調用非可重入方法。
// 可重入代碼示例
public class MathUtils {
// 純函數:輸出只依賴于輸入,沒有副作用
public static int add(int a, int b) {
return a + b;
}
// 非純函數:依賴外部狀態
private int base = 0;
public int addToBase(int value) {
return base + value; // 非可重入,依賴共享狀態
}
}
線程本地存儲(ThreadLocal)
ThreadLocal是Java中實現線程本地存儲的核心類,它為每個線程提供獨立的變量副本,避免了多線程環境下的競爭條件。
ThreadLocal的核心概念
ThreadLocal允許你將狀態與線程關聯起來,每個線程都有自己獨立初始化的變量副本。這些變量通常用于保持線程的上下文信息,如用戶會話、事務ID等。
ThreadLocal的基本使用
public class ThreadLocalExample {
// 創建ThreadLocal變量,并提供初始值
private static ThreadLocal<Integer> threadLocalCounter = ThreadLocal.withInitial(() -> 0);
private static ThreadLocal<String> threadLocalUser = new ThreadLocal<>();
public static void increment() {
threadLocalCounter.set(threadLocalCounter.get() + 1);
}
public static int getCounter() {
return threadLocalCounter.get();
}
public static void setUser(String user) {
threadLocalUser.set(user);
}
public static String getUser() {
return threadLocalUser.get();
}
public static void clear() {
// 清理ThreadLocal變量,防止內存泄漏
threadLocalCounter.remove();
threadLocalUser.remove();
}
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
// 設置線程用戶
setUser(Thread.currentThread().getName());
// 每個線程獨立計數
for (int i = 0; i < 5; i++) {
increment();
}
System.out.println(Thread.currentThread().getName() +
": Counter=" + getCounter() +
", User=" + getUser());
// 清理ThreadLocal變量
clear();
};
// 創建多個線程
Thread[] threads = new Thread[3];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(task, "Thread-" + (i + 1));
threads[i].start();
}
// 等待所有線程完成
for (Thread thread : threads) {
thread.join();
}
}
}
ThreadLocal的實現原理
ThreadLocal的實現依賴于每個Thread對象內部的ThreadLocalMap數據結構。下面是ThreadLocal的核心實現機制:
// ThreadLocal的核心方法源碼簡析
public class ThreadLocal<T> {
// 獲取當前線程的變量值
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // 獲取線程的ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue(); // 設置初始值
}
// 設置當前線程的變量值
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value); // 創建ThreadLocalMap
}
}
// 獲取與線程關聯的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 創建ThreadLocalMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
}
Thread、ThreadLocal與ThreadLocalMap的關系
ThreadLocal的實現依賴于Thread類中的兩個重要字段:
public class Thread implements Runnable {
// 線程本地變量Map
ThreadLocal.ThreadLocalMap threadLocals = null;
// 繼承自父線程的線程本地變量Map
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
// 其他字段和方法...
}
ThreadLocalMap是ThreadLocal的靜態內部類,它使用弱引用(WeakReference)作為鍵來存儲線程本地變量,這是為了避免內存泄漏。
從上圖可以看出:
- 每個Thread對象都有一個ThreadLocalMap實例
- ThreadLocalMap中存儲了多個Entry,每個Entry的鍵是ThreadLocal對象,值是線程本地變量
- 不同的ThreadLocal對象可以在不同的線程中存儲不同的值
ThreadLocal的內存泄漏問題
ThreadLocal可能引起內存泄漏,原因在于ThreadLocalMap中的Entry鍵是弱引用(WeakReference),而值是強引用:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // 鍵是弱引用
value = v; // 值是強引用
}
}
當ThreadLocal對象沒有外部強引用時,GC會回收鍵(ThreadLocal對象),但值仍然被Entry強引用,導致值無法被回收,造成內存泄漏。
解決方案:
- 使用完ThreadLocal后,及時調用remove()方法清理
- 將ThreadLocal變量聲明為static final,避免重復創建
InheritableThreadLocal:可繼承的線程本地變量
InheritableThreadLocal是ThreadLocal的子類,它允許子線程繼承父線程的線程本地變量:
public class InheritableThreadLocalExample {
private static InheritableThreadLocal<String> inheritableThreadLocal =
new InheritableThreadLocal<>();
public static void main(String[] args) {
inheritableThreadLocal.set("Parent Value");
Thread childThread = new Thread(() -> {
System.out.println("Child thread value: " + inheritableThreadLocal.get());
inheritableThreadLocal.set("Child Value");
System.out.println("Child thread value after set: " + inheritableThreadLocal.get());
});
childThread.start();
try {
childThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Parent thread value after child modification: " +
inheritableThreadLocal.get());
}
}
ThreadLocal的使用場景
- 數據庫連接管理:每個線程使用獨立的數據庫連接
- 會話管理:在Web應用中存儲用戶會話信息
- 全局參數傳遞:避免在方法參數中傳遞上下文信息
- 日期格式化:SimpleDateFormat不是線程安全的,可以使用ThreadLocal為每個線程提供獨立的實例
public class DateFormatterUtils {
private static final ThreadLocal<SimpleDateFormat> DATE_FORMATTER =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static String formatDate(Date date) {
return DATE_FORMATTER.get().format(date);
}
public static Date parseDate(String dateString) throws ParseException {
return DATE_FORMATTER.get().parse(dateString);
}
}
四、鎖優化技術
HotSpot虛擬機實現了多種鎖優化技術,提高并發性能。
4.1 自旋鎖與自適應自旋
當線程請求鎖時,如果鎖被占用,線程不立即阻塞,而是執行忙循環(自旋)等待鎖釋放。
// 自旋鎖偽代碼
public class SpinLock {
private AtomicReference<Thread> owner = new AtomicReference<>();
public void lock() {
Thread currentThread = Thread.currentThread();
// 自旋等待
while (!owner.compareAndSet(null, currentThread)) {
// 空循環,等待鎖釋放
}
}
public void unlock() {
Thread currentThread = Thread.currentThread();
owner.compareAndSet(currentThread, null);
}
}
自適應自旋:根據前一次的自旋時間和鎖擁有者的狀態動態調整自旋時間。
4.2 鎖消除
JVM通過逃逸分析檢測不可能存在共享數據競爭的鎖,并消除這些鎖。
public String concatString(String s1, String s2, String s3) {
return s1 + s2 + s3;
}
上述代碼編譯后相當于:
public String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1); // 同步方法
sb.append(s2); // 同步方法
sb.append(s3); // 同步方法
return sb.toString();
}
JVM通過逃逸分析發現sb不會逃逸出方法,自動消除鎖操作。
4.3 鎖粗化
將連續的對同一對象加鎖解鎖操作合并為一次范圍更大的加鎖操作。
// 多次加鎖解鎖
public void method() {
synchronized(lock) {
// 操作1
}
// 一些其他代碼...
synchronized(lock) {
// 操作2
}
}
// 鎖粗化后
public void method() {
synchronized(lock) {
// 操作1
// 一些其他代碼...
// 操作2
}
}
4.4 輕量級鎖
輕量級鎖減少傳統重量級鎖使用操作系統互斥量產生的性能消耗。
輕量級鎖工作流程:
4.5 偏向鎖
偏向鎖消除無競爭情況下的同步原語,偏向于第一個獲取它的線程。
偏向鎖的撤銷:
- 當對象計算過哈希碼后,無法進入偏向狀態
- 當偏向鎖收到計算一致性哈希碼請求時,撤銷偏向狀態,膨脹為重量級鎖
五、實踐建議
- 優先使用synchronized:在簡單場景下,synchronized更簡潔且性能足夠好
- 需要高級功能時使用ReentrantLock:如定時鎖等待、可中斷鎖等待、公平鎖等
- 使用讀多寫少的并發容器:如ConcurrentHashMap、CopyOnWriteArrayList等
- 使用原子類替代同步:在簡單原子操作場景下,使用AtomicInteger等原子類
- 謹慎使用線程本地存儲:避免內存泄漏,及時調用remove()方法清理
- 根據場景選擇合適鎖優化:在競爭激烈場景下,考慮禁用偏向鎖(-XX:-UseBiasedLocking)
六、總結
線程安全與鎖優化是Java并發編程的核心內容。理解線程安全的不同級別、掌握各種同步機制的原理和適用場景,能夠幫助我們編寫出更高效、更安全的并發程序。
從基本的互斥同步到非阻塞同步,從鎖消除到偏向鎖,Java虛擬機提供了豐富的線程安全保障和優化手段。作為開發者,我們應該根據具體場景選擇最合適的同步方式,在保證正確性的前提下追求更高的性能。
ThreadLocal作為實現線程安全的重要工具,通過為每個線程提供獨立的變量副本,避免了共享數據的競爭條件。然而,使用ThreadLocal時需要注意內存泄漏問題,及時清理不再需要的變量。
記住,并發編程是一門藝術,而了解底層實現原理是掌握這門藝術的基礎。只有深入理解線程安全與鎖優化的機制,才能寫出真正高效、可靠的并發程序。
?? 如果你喜歡這篇文章,請點贊支持! ?? 同時歡迎關注我的博客,獲取更多精彩內容!
本文來自博客園,作者:佛祖讓我來巡山,轉載請注明原文鏈接:http://www.rzrgm.cn/sun-10387834/p/19102763

浙公網安備 33010602011771號