JDK基礎--ThreadLocal原理分析與使用場景

ThreadLocal原理分析與使用場景

1、什麼是ThreadLocal變量

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

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

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

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

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方法:oracle

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

ThreadLocalMap是個靜態的內部類:app

static class ThreadLocalMap {   
    ........   
    }

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

3、內存泄漏問題

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

因此若是 ThreadLocal 沒有被外部強引用的狀況下,在垃圾回收的時候會被清理掉的,這樣一來 ThreadLocalMap中使用這個 ThreadLocal 的 key 也會被清理掉。可是,value 是強引用,不會被清理,這樣一來就會出現 key 爲 null 的 value。

ThreadLocalMap實現中已經考慮了這種狀況,在調用 set()、get()、remove() 方法的時候,會清理掉 key 爲 null 的記錄。

若是說會出現內存泄漏,那只有在出現了 key 爲 null 的記錄後,沒有手動調用 remove() 方法,而且以後也再也不調用 get()、set()、remove() 方法的狀況下。因此一般在調用get()方法後,便可手機調用一下remove()方法。

4、使用場景

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

  • 每一個線程須要有本身單獨的實例
  • 實例須要在多個方法中共享,但不但願被多線程共享

對於第一點,每一個線程擁有本身實例,實現它的方式不少。例如能夠在線程內部構建一個單獨的實例。ThreadLoca 能夠以很是方便的形式知足該需求。

對於第二點,能夠在知足第一點(每一個線程有本身的實例)的條件下,經過方法間引用傳遞的形式實現。ThreadLocal 使得代碼耦合度更低,且實現更優雅。

4.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;
    }

4.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](http://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html)是線程安全的,Joda time裏的DateTimeFormat也是線程安全的)。

5、碰撞解決與神奇的0x61c88647

既然ThreadLocal用map就避免不了衝突的產生

3.1 碰撞避免和解決

這裏碰撞其實有兩種類型

  1. 只有一個ThreadLocal實例的時候(上面推薦的作法),當向thread-local變量中設置多個值的時產生的碰撞,碰撞解決是經過開放定址法, 且是線性探測(linear-probe)

  2. 多個ThreadLocal實例的時候,最極端的是每一個線程都new一個ThreadLocal實例,此時利用特殊的哈希碼0x61c88647大大下降碰撞的概率, 同時利用開放定址法處理碰撞

3.2 神奇的0x61c88647

注意 0x61c88647 的利用主要是爲了多個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); 
}

注意實例變量threadLocalHashCode, 每當建立ThreadLocal實例時這個值都會累加 0x61c88647, 目的在上面的註釋中已經寫的很清楚了:爲了讓哈希碼能均勻的分佈在2的N次方的數組裏, 即 Entry[] table

下面來看一下ThreadLocal怎麼使用的這個 threadLocalHashCode 哈希碼的,下面是ThreadLocalMap靜態內部類中的set方法的部分代碼:

private Entry[] table;

        private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

key.threadLocalHashCode & (len-1)這麼用是什麼意思? 先看一下table數組的長度吧:

ThreadLocalMap中 Entry[] table 的大小(默認爲16,resize:*2)必須是2的N次方呀(len = 2^N),那 len-1 的二進制表示就是低位連續的N個1, 那 key.threadLocalHashCode & (len-1) 的值就是 threadLocalHashCode 的低N位, 這樣就能均勻的產生均勻的分佈? 我

這與fibonacci hashing(斐波那契散列法)以及黃金分割有關,具體可研究中的 6.4 節Hashing部分

參考:

http://www.jasongj.com/java/threadlocal/

When and how should I use a ThreadLocal variable?

ThreadLocal & Memory Leak

http://java.jiderhamn.se/2012/01/29/classloader-leaks-iv-threadlocal-dangers-and-why-threadglobal-may-have-been-a-more-appropriate-name/

相關文章
相關標籤/搜索