ThreadLocal詳解

想要獲取更多文章能夠訪問個人博客 - 代碼無止境html

什麼是ThreadLocal

ThreadLocal在《Java核心技術 卷一》中被稱做線程局部變量(PS:關注公衆號itweknow,回覆「Java核心技術」獲取該書),咱們能夠利用ThreadLocal建立只能由同一線程讀和寫的變量。所以就算兩個線程正在執行同一段代碼,而且這段代碼具備對ThreadLocal變量的引用,這兩個線程也沒法看到彼此的ThreadLocal變量。java

簡單使用

1.建立ThreadLocal,只須要new一個ThreadLocal對象便可。git

private ThreadLocal<String> myThreadLocal = new ThreadLocal<String>();
複製代碼

2.設置值github

myThreadLocal.set("I'm a threadLocal");
複製代碼

3.獲取值web

myThreadLocal.get();
複製代碼

4.清除,有些狀況下咱們在使用完線程局部變量後,須要即時清理,不然會致使程序運行錯誤。spring

myThreadLocal.remove();
複製代碼

假如咱們如今要利用AOP打印方法的耗時,這個時候咱們須要在@Before方法中記錄方法開始執行的時間,而後在@AfterReturning方法中打印出來耗時時間。咱們寫在切面裏的方法可能慧在多個線程中同時執行,因此此時咱們須要ThreadLocal來記錄開始執行的時間。數組

1.咱們須要在切面類中定義一個ThreadLocal。bash

private ThreadLocal<Long> threadLocal = new ThreadLocal();
複製代碼

2.在@Before方法中記錄開始時間。數據結構

long startTime = System.currentTimeMillis();
threadLocal.set(startTime);
複製代碼

3.在@AfterReturning方法中取出開始時間,並計算耗時。分佈式

long startTime = threadLocal.get();
long spendTime = System.currentTimeMillis() - startTime;
threadLocal.remove();
System.out.println("方法執行時間:" + spendTime + "ms");
複製代碼

這裏只是借這個場景和你們一塊兒熟悉一下ThreadLocal的用法,整個打印方法耗時的實現你能夠在Github上找到,若是你想了解AOP能夠參考這篇文章《使用 Spring Boot AOP 實現 Web 日誌處理和分佈式鎖》

原理解析

其實ThreadLocal是個數據結構,下面咱們就一塊兒經過源碼來剖析一下ThreadLocal的運行原理。

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    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);
}
複製代碼

上面是ThreadLocal的get()set()方法的源碼,能夠看到ThreadLocal是將值存放在ThreadLocalMap中。其實在每一個線程中都維護着一個threadLocals變量(ThreadLocalMap類型),當使用set()方法的時候其實是將值存在當前線程的threadLocals中的,調用get()方法也是從當前線程中取值的,這樣就作到了線程間的隔離。
看到這裏想必你也奇怪,在設置值和取值的時候都沒有任何與key有關的東西,那麼當一個線程有多個ThreadLocal的時候是如何作到一一對應的呢?那咱們就一塊兒來看下這個ThreadLocalMap類吧。

static class ThreadLocalMap {
    /** * The initial capacity -- MUST be a power of two. */
    private static final int INITIAL_CAPACITY = 16;

    /** * The table, resized as necessary. * table.length MUST always be a power of two. */
    private Entry[] table;

    /** * The number of entries in the table. */
    private int size = 0;

    /** * The next size value at which to resize. */
    private int threshold; // Default to 0
}

複製代碼

由上面可見在ThreadLocalMap中維護着tablesize以及threshold三個屬性。table是一個Entry數組主要用來保存具體的數據,sizetable的大小,而threshold這表示當table中元素數量超過該值時,table就會擴容。瞭解了ThreadLocalMap的結構以後,咱們就來看下其set方法吧。

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();
}
複製代碼

經過上面的代碼分析得出,整個的設值過程以下:

  1. 經過ThreadLocal的threadLocalHashCode值定位到table中的位置i。
  2. 若是table中i這個位置是空的,那麼就新建立一個Entry對象放置在i這個位置。
  3. 若是table中i這個位置不爲空,則取出來i這個位置的key。
  4. 若是這個key恰好就是當前ThreadLocal對象,則直接修改該位置上Entry對象的value。
  5. 若是這個key不是當前TreadLocal對象,則尋找下一個位置的Entry對象,而後重複上述步驟進行判斷。

對於get方法也是一樣的原理從ThreadLocalMap中獲取值。那麼ThreadLocal是如何生成threadLocalHashCode值的呢?

public class ThreadLocal<T> {
    private final int threadLocalHashCode = nextHashCode();
    private static final int HASH_INCREMENT = 0x61c88647;
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
}
複製代碼

可見咱們在初始化一個ThreadLocal對象的時候都爲其會生成一個threadLocalHashCode值,每初始化一個ThreadLocal該值就增長0x61c88647。這樣就能夠作到每一個ThreadLocal在ThreadLocalMap中找到一個存儲值的位置了。

結束語

在文章的最後分享一次以前遇到的一個與ThreadLocal有關的坑,有一次在寫分頁的時候使用了PageHeler插件,引包的時候錯誤地引用了MybatisPlus下的PagerHelper,而MybatisPlus下的PageHelper在ThreadLocal中存儲了SQL分頁信息在使用以後沒有移除,因此執行了分頁的SQL以後在當前線程中執行的SQL都會出現問題。因此你們在使用ThreadLocal的過程當中千萬要注意在適當的時候須要清除。本文主要介紹了Java中的線程局部變量ThreadLocal的使用,而且和你們一塊兒稍微瞭解了一下源碼。但願對你們可以有所幫助。

PS:學習不止,碼不停蹄!若是您喜歡個人文章,就關注我吧!

掃碼關注「代碼無止境」
相關文章
相關標籤/搜索