首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 線程同步與 ThreadLocal 無鎖線程隔離實戰

     編輯:Java 相關知識

synchronized 關鍵字深度解析

1. 三種鎖定目標

  • 實例方法:鎖當前對象 this。
  • 靜態方法:鎖當前類的 Class 對象。
  • 同步代碼塊:鎖指定對象 synchronized(obj)。

2. 鎖規則與內存語義

  • 同一把鎖下,同一時刻僅允許一個線程進入同步區。
  • 線程進入同步塊獲取鎖 → 清空工作內存 → 從主內存讀取共享變量;退出同步塊釋放鎖 → 將工作內存共享變量刷回主內存 (happens-before)。
  • 非同步代碼不受影響,可並發訪問。

3. 鎖升級與優化 (JDK 6+)

  • 偏向鎖:無競爭時,鎖偏向首個獲取線程,後續進出無 CAS 開銷。
  • 輕量級鎖:輕度競爭,CAS 自旋獲取鎖,不進入內核。
  • 重量級鎖:激烈競爭,線程阻塞進入內核等待。
  • 鎖消除:JIT 發現無共享數據競爭,直接移除同步。
  • 鎖粗化:連續對同一對象加鎖解鎖,合併為一個大同步塊。

4. 死鎖演示與避免

// 死鎖:線程 1 鎖 A 再鎖 B,線程 2 鎖 B 再鎖 A
Object lockA = new Object(), lockB = new Object();
new Thread(() -> { synchronized(lockA){ sleep(100); synchronized(lockB){} } }).start();
new Thread(() -> { synchronized(lockB){ sleep(100); synchronized(lockA){} } }).start();

避免策略:固定加鎖順序、使用 tryLock(timeout) (ReentrantLock)、避免在鎖內調用外部方法。

ThreadLocal 線程隔離原理與最佳實踐

1. 核心結構

  • 每個 Thread 內部持有 ThreadLocalMap (類似 HashMap,Key 為 ThreadLocal 對象弱引用,Value 為任意對象)。
  • ThreadLocal.set(value):獲取當前線程 Map → 以 this 為 Key 存入 Value。
  • ThreadLocal.get():獲取當前線程 Map → 以 this 為 Key 取值,無則調用 initialValue()。
  • ThreadLocal.remove():從當前線程 Map 刪除 Key,防內存洩漏。

2. 典型應用場景

  1. 數據庫連接/事務管理:Spring TransactionSynchronizationManager 用 ThreadLocal 綁定 Connection,保證同一事務內多 DAO 共用同一連接。
  2. 用戶上下文傳遞:Web 請求中存放用戶 ID、租戶 ID、追蹤 ID,避免層層傳參。
  3. 非線程安全對象隔離:SimpleDateFormat、Random、JDBC Statement 等每線程一份,無需同步。
  4. 跨層參數透傳:框架內部攔截器、切面、過濾器間共享請求級數據。

3. 代碼範例:請求上下文持有者

public final class RequestContextHolder {
    private static final ThreadLocal<RequestContext> CONTEXT_HOLDER = new ThreadLocal<>();

    public static void setContext(RequestContext ctx) {
        CONTEXT_HOLDER.set(ctx);
    }

    public static RequestContext getContext() {
        return CONTEXT_HOLDER.get();
    }

    public static void clear() {
        CONTEXT_HOLDER.remove(); // 必須調用,防洩漏
    }
}

// 攔截器中使用
public class ContextInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        RequestContext ctx = new RequestContext();
        ctx.setUserId(getUserId(request));
        ctx.setTraceId(UUID.randomUUID().toString());
        RequestContextHolder.setContext(ctx);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        RequestContextHolder.clear(); // 關鍵:請求結束清理
    }
}

4. 內存洩漏陷阱與防範

原因:ThreadLocalMap Key 為弱引用,Value 強引用。線程池線程長期存活,若不 remove(),Key GC 後 Value 仍被 Entry 強引用 → 無法回收 → OOM。

防範:

  • 使用 try-finally 確保 remove() 執行。
  • 框架層面在請求結束/線程歸還池時統一清理 (如 Spring RequestContextListener、Netty FastThreadLocal)。
  • 避免在靜態字段存放大對象 Value,改用弱引用 Value 或定期掃描清理。

5. InheritableThreadLocal 子線程傳遞

父線程創建子線程時自動複製父線程的 InheritableThreadLocal 值。注意:線程池複用線程不會觸發複製,需配合 ThreadPoolTaskExecutor 的 ThreadFactory 或 RunnableWrapper 手動傳遞。

對比總結

維度synchronized / LockThreadLocal
核心目標互斥訪問共享資源線程隔離,各線程獨享副本
性能開銷上下文切換、CAS、內核轉換極低 (Map 讀寫),無阻塞
適用場景計數器、共享集合、複雜業務臨界區連接綁定、上下文傳遞、非線程安全工具類隔離
內存風險無特殊風險線程池長生命周期導致 Value 洩漏
組合使用保護 ThreadLocal Map 內部操作 (JDK 內部已處理)配合 synchronized 保護共享初始化邏輯

最佳實踐清單

  1. 優先使用無鎖數據結構 (ConcurrentHashMap、LongAdder) 替代同步。
  2. 同步塊保持最小範圍,避免在鎖內做 I/O、RPC、睡眠。
  3. ThreadLocal 必須配對 set/remove,嵌套調用用 try-finally。
  4. 線程池場景考慮 TransmittableThreadLocal (阿里開源) 實現上下文傳遞。
  5. 定期用 Arthas/JVisualVM 掃描 ThreadLocalMap 洩漏。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved