談談你對ThreadLocal的理解

1. 你知道ThreadLocal是什麼嗎?

簡單地說,就是用來隔離數據的。用ThreadLocal來保存的數據,只對當前線程生效,當前線程對該數據做的任何操作,對別的線程是不生效的。舉個栗子一看便知:

public class TestThreadLocal {

    private static User user = new User("jerry", "123"));

    public void fun(User user){
        System.out.println(Thread.currentThread().getName() + "開始執行,修改前的username[" + user.getUsername() + "]");
        user.setUsername("tom");
        System.out.println(Thread.currentThread().getName() + "執行完畢,修改後的username[" + user.getUsername() + "]");
    }

    public static void main(String[] args) {
        TestThreadLocal testThreadLocal = new TestThreadLocal();
        new Thread(() -> {
            testThreadLocal.fun(user);
        }, "線程A").start();

        new Thread(() -> {
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
            testThreadLocal.fun(user);
        }, "線程B").start();
    }
}

這段代碼很簡單,就是有個user,初始的username爲jerry,然後線程A將其改名爲tom,3秒鐘後,線程B再去讀取user的username。因爲線程A將起改爲tom了,所以線程B開始執行時讀取到的應該是tom。看執行結果:

分析得沒錯,線程B開始執行時讀取到的確實是tom,說明線程A的修改對線程B生效了。假如用ThreadLocal,結果就不是這樣了。

怎麼用呢?首先將上述代碼中的:

private static User user = new User("jerry", "123"));

改成:

private static ThreadLocal<User> user = ThreadLocal.withInitial(() -> new User("jerry", "123"));

然後將testThreadLocal.fun(user);改成testThreadLocal.fun(user.get());,這樣就可以了,再次執行,結果如下:

2. 說說ThreadLocal的常用方法

  • set(T t):將值綁定到當前線程中。注意是當前線程,比如你在main線程中set值,再另起兩個線程去get,是取不到的;

  • get():當前線程從ThreadLocal中取出自己的副本;

假如你定義了一個靜態變量:

private static ThreadLocal<User> threadLocal = new ThreadLocal<>();

那麼你必須在線程A和線程B中分別進行set,而不能在主線程中進行set,如下:

new Thread(() -> {
    threadLocal.set(new User("jerry", "123"));
    testThreadLocal.fun(threadLocal.get());
}, "線程A").start();

new Thread(() -> {
    threadLocal.set(new User("jerry", "123"));
    try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
    testThreadLocal.fun(threadLocal.get());
}, "線程B").start();

jdk8提供了新的構造方式,即開篇中用到的那種方式:

private static ThreadLocal<User> user = ThreadLocal.withInitial(() -> new User("jerry", "123"));

這樣就不用在每個線程中進行set了,在這裏初始化一次,每個線程get時都會取出屬於自己的副本。

3. 對ThreadLocal的應用場景有了解嗎?

  • 很多框架中用到了ThreadLocal,比如Spring中,用ThreadLocal來保存數據庫連接,這樣可以保證單個線程的操作使用的是同一個數據庫連接;

  • 可以用ThreadLocal來做session、cookie的隔離;

  • 最經典的一個,SimpleDataFormat調用parse格式化時間的時候,parse方法會先調用Calendar.clear(),再調用Calendar.add(),如果一個線程調用了調用完add,在準備繼續parse的時候,另一個線程clear掉了,這就出問題了,所以可以用ThreadLocal;

  • 還有一個就是可以用來傳參數,比如你多個方法都要對user對象進行操作,並且有些方法是第三方jar的,不能把user當成參數傳過去,那麼就可以將user裝到ThreadLocal中,要用的時候get出來即可。

4. ThreadLocal的底層原理你造不?

來看看ThreadLocal的set和get方法就秒懂了:

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

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();
}

看到這裏是不是就一目瞭然呢,其實ThreadLocal裏面有個ThreadLocalMap,把當前線程作爲key,就可以拿到這個線程專屬的ThreadLocalMap,所以說每個線程都有一個ThreadLocalMap,拿到了這個ThreadLocalMap後(如果拿到的不爲空,就用這個,爲空就創建一個,保證每個線程只有一個),將當前ThreadLocal綁定的值設置到map中;get的時候就將map的entry.value返回,就是我們存入的對象啦。

5. 那你對ThreadLocalMap瞭解過嗎?

上面說了,每個線程只對應一個ThreadLocalMap,但是一個線程可以有很多個ThreadLocal來保存不同的對象,那麼ThreadLocalMap怎麼來保存這多個ThreadLocal呢?那就是用你數組!源碼中有這麼一個數組:

private Entry[] table;

這個就是用來保存ThreadLocal的。怎麼判斷當前新加入的ThreadLocal放在數組的哪個位置呢?索引怎麼計算出來?ThreadLocalMap會利用usafe類計算出一個threadLocalHashCode,然後再根據:

int i = key.threadLocalHashCode & (len-1)

計算出來的i就是存放當前ThreadLocal的索引。

6. 你用ThreadLocal遇到過什麼問題嗎?

首先來看看ThreadLocalMap中的這段代碼:

static class Entry extends WeakReference<ThreadLocal<?>> {
      /** The value associated with this ThreadLocal. */
      Object value;

      Entry(ThreadLocal<?> k, Object v) {
             super(k);
             value = v;
      }
}

ThreadLocalMap中的key,也就是ThreadLocal對象,被設計成弱引用了,所以在外部沒有強引用key的時候,key會被垃圾回收清理掉,ThreadLocalMap中就會出現key爲null的Entry,永遠無法被GC回收,從而造成內存泄漏。爲了避免這個問題,用完ThreadLocal最好調用一下remove方法,手動清除掉。

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