ThreadLocal學習與使用

1:什麼是ThreadLocal

ThreadLoal 變量,線程局部變量,同一個 ThreadLocal 所包含的對象,在不同的 Thread 中有不同的副本。這裏有幾點需要注意:

  • 因爲每個 Thread 內有自己的實例副本,且該副本只能由當前 Thread 使用。這是也是 ThreadLocal 命名的由來。
  • 既然每個 Thread 有自己的實例副本,且其它 Thread 不可訪問,那就不存在多線程間共享的問題。

ThreadLocal 提供了線程本地的實例。它與普通變量的區別在於,每個使用該變量的線程都會初始化一個完全獨立的實例副本。ThreadLocal 變量通常被private static修飾。當一個線程結束時,它所使用的所有 ThreadLocal 相對的實例副本都可被回收。

總的來說,ThreadLocal 適用於每個線程需要自己獨立的實例且該實例需要在多個方法中被使用,也即變量在線程間隔離而在方法或類間共享的場景。

2:ThreadLocal實現原理

首先 ThreadLocal 是一個泛型類,保證可以接受任何類型的對象。

因爲一個線程內可以存在多個 ThreadLocal 對象,所以其實是 ThreadLocal 內部維護了一個 Map ,這個 Map 不是直接使用的 HashMap ,而是 ThreadLocal 實現的一個叫做 ThreadLocalMap 的靜態內部類。而我們使用的 get()、set() 方法其實都是調用了這個ThreadLocalMap類對應的 get()、set() 方法。例如下面的 set 方法:

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

get方法:

    public T get() {   
        Thread t = Thread.currentThread();   
        ThreadLocalMap map = getMap(t);   
        if (map != null)   
            return (T)map.get(this);   
  
        // Maps are constructed lazily.  if the map for this thread   
        // doesn't exist, create it, with this ThreadLocal and its   
        // initial value as its only entry.   
        T value = initialValue();   
        createMap(t, value);   
        return value;   
    }

createMap方法:

    void createMap(Thread t, T firstValue) {   
        t.threadLocals = new ThreadLocalMap(this, firstValue);   
    }

最終的變量是放在了當前線程的 ThreadLocalMap 中,並不是存在 ThreadLocal 上,ThreadLocal 可以理解爲只是ThreadLocalMap的封裝,傳遞了變量值。

如何實現一個線程多個ThreadLocal對象,每一個ThreadLocal對象是如何區分的呢?

private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
      return nextHashCode.getAndAdd(HASH_INCREMENT);
}

對於每一個ThreadLocal對象,都有一個final修飾的int型的threadLocalHashCode不可變屬性,對於基本數據類型,可以認爲它在初始化後就不可以進行修改,所以可以唯一確定一個ThreadLocal對象。
  但是如何保證兩個同時實例化的ThreadLocal對象有不同的threadLocalHashCode屬性:在ThreadLocal類中,還包含了一個static修飾的AtomicInteger([əˈtɒmɪk]提供原子操作的Integer類)成員變量(即類變量)和一個static final修飾的常量(作爲兩個相鄰nextHashCode的差值)。由於nextHashCode是類變量,所以每一次調用ThreadLocal類都可以保證nextHashCode被更新到新的值,並且下一次調用ThreadLocal類這個被更新的值仍然可用,同時AtomicInteger保證了nextHashCode自增的原子性。

3:內存泄漏問題

實際上 ThreadLocalMap 中使用的 key 爲 ThreadLocal 的弱引用,弱引用的特點是,如果這個對象只存在弱引用,那麼在下一次垃圾回收的時候必然會被清理掉。

所以如果 ThreadLocal 沒有被外部強引用的情況下,在垃圾回收的時候會被清理掉的,這樣一來 ThreadLocalMap中使用這個 ThreadLocal 的 key 也會被清理掉。但是,value 是強引用,不會被清理,這樣一來就會出現 key 爲 null 的 value。

ThreadLocalMap實現中已經考慮了這種情況,在調用 set()、get()、remove() 方法的時候,會清理掉 key 爲 null 的記錄。如果說會出現內存泄漏,那只有在出現了 key 爲 null 的記錄後,沒有手動調用 remove() 方法,並且之後也不再調用 get()、set()、remove() 方法的情況下。

ThreadLocalMap設計時的對上面問題的對策:
ThreadLocalMap的getEntry函數的流程大概爲:

  1. 首先從ThreadLocal的直接索引位置(通過ThreadLocal.threadLocalHashCode & (table.length-1)運算得到)獲取Entry e,如果e不爲null並且key相同則返回e;
  2. 如果e爲null或者key不一致則向下一個位置查詢,如果下一個位置的key和當前需要查詢的key相等,則返回對應的Entry。否則,如果key值爲null,則擦除該位置的Entry,並繼續向下一個位置查詢。在這個過程中遇到的key爲null的Entry都會被擦除,那麼Entry內的value也就沒有強引用鏈,自然會被回收。仔細研究代碼可以發現,set操作也有類似的思想,將key爲null的這些Entry都刪除,防止內存泄露。
      但是光這樣還是不夠的,上面的設計思路依賴一個前提條件:要調用ThreadLocalMap的getEntry函數或者set函數。這當然是不可能任何情況都成立的,所以很多情況下需要使用者手動調用ThreadLocal的remove函數,手動刪除不再需要的ThreadLocal,防止內存泄露。所以JDK建議將ThreadLocal變量定義成private static的,這樣的話ThreadLocal的生命週期就更長,由於一直存在ThreadLocal的強引用,所以ThreadLocal也就不會被回收,也就能保證任何時候都能根據ThreadLocal的弱引用訪問到Entry的value值,然後remove它,防止內存泄露。

即:
1.使用ThreadLocal,建議用static修飾 static ThreadLocal headerLocal = new ThreadLocal();
2.使用完ThreadLocal後,執行remove操作,避免出現內存溢出情況。

4:使用場景

如上文所述,ThreadLocal 適用於如下兩種場景

  • 每個線程需要有自己單獨的實例
  • 實例需要在多個方法中共享,但不希望被多線程共享

對於第一點,每個線程擁有自己實例,實現它的方式很多。例如可以在線程內部構建一個單獨的實例。ThreadLoca 可以以非常方便的形式滿足該需求。

對於第二點,可以在滿足第一點(每個線程有自己的實例)的條件下,通過方法間引用傳遞的形式實現。ThreadLocal 使得代碼耦合度更低,且實現更優雅。

1)存儲用戶Session

一個簡單的用ThreadLocal來存儲Session的例子:

    private static final ThreadLocal threadSession = new ThreadLocal();

    public static Session getSession() throws InfrastructureException {
        Session s = (Session) threadSession.get();
        try {
            if (s == null) {
                s = getSessionFactory().openSession();
                threadSession.set(s);
            }
        } catch (HibernateException ex) {
            throw new InfrastructureException(ex);
        }
        return s;
    }

2)解決線程安全的問題

比如Java7中的SimpleDateFormat不是線程安全的,可以用ThreadLocal來解決這個問題:

public class DateUtil {
    private static ThreadLocal<SimpleDateFormat> format1 = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static String formatDate(Date date) {
        return format1.get().format(date);
    }
}

這裏的DateUtil.formatDate()就是線程安全的了。(Java8裏的 java.time.format.DateTimeFormatter是線程安全的)。

3) PageHelper中保存Page信息的時候使用了ThreadLocal

public abstract class PageMethod {
    protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
    protected static boolean DEFAULT_COUNT = true;

    protected static void setLocalPage(Page page) {
        LOCAL_PAGE.set(page);
    }
   
    public static <T> Page<T> getLocalPage() {
        return LOCAL_PAGE.get();
    }
    public static void clearPage() {
        LOCAL_PAGE.remove();
    }
 }

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章