輕鬆使用線程: 不共享有時是最好的

ThreadLocal 類是悄悄地出如今 Java 平臺版本 1.2 中的。雖然支持線程局部變量早就是許多線程工具(例如 Posix pthreads 工具)的一部分,但 Java Threads API 的最初設計卻沒有這項有用的功能。並且,最初的實現也至關低效。因爲這些緣由, ThreadLocal 極少受到關注,但對簡化線程安全併發程序的開發來講,它倒是很方便的。在 輕鬆使用線程的第 3 部分,Java 軟件顧問 Brian Goetz 研究了 ThreadLocal 並提供了一些使用技巧。
編寫線程安全類是困難的。它不但要求仔細分析在什麼條件能夠對變量進行讀寫,並且要求仔細分析其它類能如何使用某個類。 有時,要在不影響類的功能、易用性或性能的狀況下使類成爲線程安全的是很困難的。有些類保留從一個方法調用到下一個方法調用的狀態信息,要在實踐中使這樣的類成爲線程安全的是困難的。
管理非線程安全類的使用比試圖使類成爲線程安全的要更容易些。非線程安全類一般能夠安全地在多線程程序中使用,只要您能確保一個線程所用的類的實例不被其它線程使用。例如,JDBC Connection 類是非線程安全的 — 兩個線程不能在小粒度級上安全地共享一個 Connection — 但若是每一個線程都有它本身的 Connection ,那麼多個線程就能夠同時安全地進行數據庫操做。
不使用 ThreadLocal 爲每一個線程維護一個單獨的 JDBC 鏈接(或任何其它對象)固然是可能的;Thread API 給了咱們把對象和線程聯繫起來所需的全部工具。而 ThreadLocal 則使咱們能更容易地把線程和它的每線程(per-thread)數據成功地聯繫起來。
線程局部變量高效地爲每一個使用它的線程提供單獨的線程局部變量值的副本。每一個線程只能看到與本身相聯繫的值,而不知作別的線程可能正在使用或修改它們本身的副本。一些編譯器(例如 Microsoft Visual C++ 編譯器或 IBM XL FORTRAN 編譯器)用存儲類別修飾符(像 staticvolatile )把對線程局部變量的支持集成到了其語言中。Java 編譯器對線程局部變量不提供特別的語言支持;相反地,它用 ThreadLocal 類實現這些支持, 核心 Thread 類中有這個類的特別支持。
由於線程局部變量是經過一個類來實現的,而不是做爲 Java 語言自己的一部分,因此 Java 語言線程局部變量的使用語法比內建線程局部變量語言的使用語法要笨拙一些。要建立一個線程局部變量,請實例化類 ThreadLocal 的一個對象。 ThreadLocal 類的行爲與 java.lang.ref 中的各類 Reference 類的行爲很類似; ThreadLocal 類充當存儲或檢索一個值時的間接句柄。清單 1 顯示了 ThreadLocal 接口。

清單 1. ThreadLocal 接口
public class ThreadLocal { 
  public Object get();
  public void set(Object newValue);
  public Object initialValue();
}

get() 訪問器檢索變量的當前線程的值; set() 訪問器修改當前線程的值。 initialValue() 方法是可選的,若是線程未使用過某個變量,那麼您能夠用這個方法來設置這個變量的初始值;它容許延遲初始化。用一個示例實現來講明 ThreadLocal 的工做方式是最好的方法。清單 2 顯示了 ThreadLocal 的一個實現方式。它不是一個特別好的實現(雖然它與最初實現很是類似),因此極可能性能不佳,但它清楚地說明了 ThreadLocal 的工做方式。

清單 2. ThreadLocal 的糟糕實現
public class ThreadLocal { 
  private Map values = Collections.synchronizedMap(new HashMap());
  public Object get() {
    Thread curThread = Thread.currentThread();
    Object o = values.get(curThread);
    if (o == null && !values.containsKey(curThread)) {
      o = initialValue();
      values.put(curThread, o);
    }
    return o;
  }
  public void set(Object newValue) {
    values.put(Thread.currentThread(), newValue);
  }
  public Object initialValue() {
    return null;
  }
}

這個實現的性能不會很好,由於每一個 get()set() 操做都須要 values 映射表上的同步,並且若是多個線程同時訪問同一個 ThreadLocal ,那麼將發生爭用。此外,這個實現也是不切實際的,由於用 Thread 對象作 values 映射表中的關鍵字將致使沒法在線程退出後對 Thread 進行垃圾回收,並且也沒法對死線程的 ThreadLocal 的特定於線程的值進行垃圾回收。




回頁首


線程局部變量常被用來描繪有狀態「單子」(Singleton) 或線程安全的共享對象,或者是經過把不安全的整個變量封裝進 ThreadLocal ,或者是經過把對象的特定於線程的狀態封裝進 ThreadLocal 。例如,在與數據庫有緊密聯繫的應用程序中,程序的不少方法可能都須要訪問數據庫。在系統的每一個方法中都包含一個 Connection 做爲參數是不方便的 — 用「單子」來訪問鏈接多是一個雖然更粗糙,但卻方便得多的技術。然而,多個線程不能安全地共享一個 JDBC Connection 。如清單 3 所示,經過使用「單子」中的 ThreadLocal ,咱們就能讓咱們的程序中的任何類容易地獲取每線程 Connection 的一個引用。這樣,咱們能夠認爲 ThreadLocal 容許咱們建立 每線程單子

清單 3. 把一個 JDBC 鏈接存儲到一個每線程 Singleton 中
public class ConnectionDispenser { 
  private static class ThreadLocalConnection extends ThreadLocal {
    public Object initialValue() {
      return DriverManager.getConnection(ConfigurationSingleton.getDbUrl());
    }
  }
  private ThreadLocalConnection conn = new ThreadLocalConnection();
  public static Connection getConnection() {
    return (Connection) conn.get();
  }
}

任何建立的花費比使用的花費相對昂貴些的有狀態或非線程安全的對象,例如 JDBC Connection 或正則表達式匹配器,都是可使用每線程單子(singleton)技術的好地方。固然,在相似這樣的地方,您可使用其它技術,例如用池,來安全地管理共享訪問。然而,從可伸縮性角度看,即便是用池也存在一些潛在缺陷。由於池實現必須使用同步,以維護池數據結構的完整性,若是全部線程使用同一個池,那麼在有不少線程頻繁地對池進行訪問的系統中,程序性能將因爭用而下降。




回頁首


其它適合使用 ThreadLocal 但用池卻不能成爲很好的替代技術的應用程序包括存儲或累積每線程上下文信息以備稍後檢索之用這樣的應用程序。例如,假設您想建立一個用於管理多線程應用程序調試信息的工具。您能夠用如清單 4 所示的 DebugLogger 類做爲線程局部容器來累積調試信息。在一個工做單元的開頭,您清空容器,而當一個錯誤出現時,您查詢該容器以檢索這個工做單元迄今爲止生成的全部調試信息。

清單 4. 用 ThreadLocal 管理每線程調試日誌
public class DebugLogger {
  private static class ThreadLocalList extends ThreadLocal {
    public Object initialValue() {
      return new ArrayList();
    }
    public List getList() { 
      return (List) super.get(); 
    }
  }
  private ThreadLocalList list = new ThreadLocalList();
  private static String[] stringArray = new String[0];
  public void clear() {
    list.getList().clear();
  }
  public void put(String text) {
    list.getList().add(text);
  }
  public String[] get() {
    return list.getList().toArray(stringArray);
  }
}

在您的代碼中,您能夠調用 DebugLogger.put() 來保存您的程序正在作什麼的信息,並且,稍後若是有必要(例如發生了一個錯誤),您可以容易地檢索與某個特定線程相關的調試信息。 與簡單地把全部信息轉儲到一個日誌文件,而後努力找出哪一個日誌記錄來自哪一個線程(還要擔憂線程爭用日誌紀錄對象)相比,這種技術簡便得多,也有效得多。
ThreadLocal 在基於 servlet 的應用程序或工做單元是一個總體請求的任何多線程應用程序服務器中也是頗有用的,由於在處理請求的整個過程當中將要用到單個線程。您能夠經過前面講述的每線程單子技術用 ThreadLocal 變量來存儲各類每請求(per-request)上下文信息。




回頁首


ThreadLocal 類有一個親戚,InheritableThreadLocal,它以類似的方式工做,但適用於種類徹底不一樣的應用程序。建立一個線程時若是保存了全部 InheritableThreadLocal 對象的值,那麼這些值也將自動傳遞給子線程。若是一個子線程調用 InheritableThreadLocalget() ,那麼它將與它的父線程看到同一個對象。爲保護線程安全性,您應該只對不可變對象(一旦建立,其狀態就永遠不會被改變的對象)使用 InheritableThreadLocal ,由於對象被多個線程共享。 InheritableThreadLocal 很合適用於把數據從父線程傳到子線程,例如用戶標識(user id)或事務標識(transaction id),但不能是有狀態對象,例如 JDBC Connection




回頁首


雖然線程局部變量早已赫赫有名並被包括 Posix pthreads 規範在內的不少線程框架支持,但最初的 Java 線程設計中卻省略了它,只是在 Java 平臺的版本 1.2 中才添加上去。在不少方面, ThreadLocal 仍在發展之中;在版本 1.3 中它被重寫,版本 1.4 中又重寫了一次,兩次都專門是爲了性能問題。
在 JDK 1.2 中, ThreadLocal 的實現方式與清單 2 中的方式很是類似,除了用同步 WeakHashMap 代替 HashMap 來存儲 values 以外。(以一些額外的性能開銷爲代價,使用 WeakHashMap 解決了沒法對 Thread 對象進行垃圾回收的問題。)不用說, ThreadLocal 的性能是至關差的。
Java 平臺版本 1.3 提供的 ThreadLocal 版本已經儘可能更好了;它不使用任何同步,從而不存在可伸縮性問題,並且它也不使用弱引用。相反地,人們經過給 Thread 添加一個實例變量(該變量用於保存當前線程的從線程局部變量到它的值的映射的 HashMap )來修改 Thread 類以支持 ThreadLocal 。由於檢索或設置一個線程局部變量的過程不涉及對可能被另外一個線程讀寫的數據的讀寫操做,因此您能夠不用任何同步就實現 ThreadLocal.get()set() 。並且,由於每線程值的引用被存儲在自已的 Thread 對象中,因此當對 Thread 進行垃圾回收時,也能對該 Thread 的每線程值進行垃圾回收。
不幸的是,即便有了這些改進,Java 1.3 中的 ThreadLocal 的性能仍然出奇地慢。據個人粗略測量,在雙處理器 Linux 系統上的 Sun 1.3 JDK 中進行 ThreadLocal.get() 操做,所耗費的時間大約是無爭用同步的兩倍。性能這麼差的緣由是 Thread.currentThread() 方法的花費很是大,佔了 ThreadLocal.get() 運行時間的三分之二還多。雖然有這些缺點,JDK 1.3 ThreadLocal.get() 仍然比爭用同步快得多,因此若是在任何存在嚴重爭用的地方(多是有很是多的線程,或者同步塊被頻繁地執行,或者同步塊很大), ThreadLocal 可能仍然要高效得多。
在 Java 平臺的最新版本,即版本 1.4b2 中, ThreadLocalThread.currentThread() 的性能都有了很大提升。有了這些提升, ThreadLocal 應該比其它技術,如用池,更快。因爲它比其它技術更簡單,也更不易出錯,人們最終將發現它是避免線程間出現不但願的交互的有效途徑。




回頁首


ThreadLocal 能帶來不少好處。它經常是把有狀態類描繪成線程安全的,或者封裝非線程安全類以使它們可以在多線程環境中安全地使用的最容易的方式。使用 ThreadLocal 使咱們能夠繞過爲實現線程安全而對什麼時候須要同步進行判斷的複雜過程,並且由於它不須要任何同步,因此也改善了可伸縮性。除簡單以外,用 ThreadLocal 存儲每線程單子或每線程上下文信息在歸檔方面還有一個很有價值好處 — 經過使用 ThreadLocal ,存儲在 ThreadLocal 中的對象都是 被線程共享的是清晰的,從而簡化了判斷一個類是否線程安全的工做。
相關文章
相關標籤/搜索