扒一扒ThreadLocal原理及應用

先總述,後分析

深挖過ThreadLocal之後,一句話概括:Synchronized用於線程間的數據共享,而ThreadLocal則用於線程間的數據隔離。所以ThreadLocal的應用場合,最適合的是按線程多實例(每個線程對應一個實例)的對象的訪問,並且這個對象很多地方都要用到。
  數據隔離的祕訣其實是這樣的,Thread有個TheadLocalMap類型的屬性,叫做threadLocals,該屬性用來保存該線程本地變量。這樣每個線程都有自己的數據,就做到了不同線程間數據的隔離,保證了數據安全。
  接下來採用jdk1.8源碼進行深挖一下TheadLocal和TheadLocalMap。

1、ThreadLocal是什麼?

ThreadLocal爲解決多線程程序的併發問題提供了一種新的思路。使用這個工具類可以很簡潔地編寫出優美的多線程程序。
  當使用ThreadLocal維護變量時,ThreadLocal爲每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。
  從線程的角度看,目標變量就象是線程的本地變量,這也是類名中“Local”所要表達的意思。
  所以,在Java中編寫線程局部變量的代碼相對來說要笨拙一些,因此造成線程局部變量沒有在Java開發者中得到很好的普及。

2、ThreadLocal原理?

既然ThreadLocal則用於線程間的數據隔離,每個線程都可以獨立的操作自己獨立的變量副本而不會影響別的線程中的變量。先用一個簡單的代碼演示一下結論:

public class threadLocalDemo {
    static ThreadLocal<Person> threadLocal = new ThreadLocal<Person>();
    public static void main(String[] args) {
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(threadLocal.get());
        },"t1").start();

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            threadLocal.set(new Person());
        },"t2").start();
    }
    static class Person {
        String name = "yangguo";
    }

運行結果:null

一個簡單的ThreadLocal演示,開啓兩個線程t1、t2,線程t2對threadLocal進行了set,但是並沒有改變線程t1本地的threadlocal變量值。

扒扒底層源碼看看到底做了什麼?進入threadLocal.set()方法,源碼如下:

 public void set(T value) {
        Thread t = Thread.currentThread();   //獲取當前線程
        ThreadLocalMap map = getMap(t);      //根據當前線程獲取ThreadLocalMap,getMap()源碼接在下面
        if (map != null) 
            map.set(this, value);           //給ThreadLocalMap賦值(ThreadLocal,value)
        else
            createMap(t, value);
    }

---------------**getMap(t)源碼**-----------------------
 ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;       //從當前Thread類中獲取到ThreadLocalMap
    }
------------**map.set源碼:將map中的key,value組裝成一個Entry,而在Entry中繼承了WeakReference弱引用**------
 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();
        }
------------------------**Entry源碼:繼承了弱引用,**--------------------------
static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

Thread內部變量ThreadLocalMap
扒完源碼之後整理一下threadLocal中set方法的過程。

  1. 獲取到當前線程
  2. 從當前線程中獲取到自己內部的ThreadLocalMap,別的線程無法訪問這個map
  3. 往ThreadLocalMap值塞值(ThreadLocal,value)
  4. 塞值的過程中將key和value組裝成了一個Entry,繼承了弱引用。防止ThreadLocal引用的對象內存泄漏。

所以對於不同的線程,每次獲取副本值時,別的線程並不能獲取到當前線程的副本值,形成了副本的隔離,互不干擾。

ThreadLocal的內部結構圖如下:
ThreadLocal結構內部

3、ThreadLocal類的核心方法

ThreadLocal類核心方法set、get、initialValue、withInitial、setInitialValue、remove

/**
     * Returns the current thread's "initial value" for this
     * thread-local variable.  This method will be invoked the first
     * time a thread accesses the variable with the {@link #get}
     * method, unless the thread previously invoked the {@link #set}
     * method, in which case the {@code initialValue} method will not
     * be invoked for the thread.  Normally, this method is invoked at
     * most once per thread, but it may be invoked again in case of
     * subsequent invocations of {@link #remove} followed by {@link #get}.
     *
     * <p>This implementation simply returns {@code null}; if the
     * programmer desires thread-local variables to have an initial
     * value other than {@code null}, {@code ThreadLocal} must be
     * subclassed, and this method overridden.  Typically, an
     * anonymous inner class will be used.
     *
     * @return the initial value for this thread-local
     */
    protected T initialValue() {
        return null;
    }

    /**
     * Creates a thread local variable. The initial value of the variable is
     * determined by invoking the {@code get} method on the {@code Supplier}.
     *
     * @param <S> the type of the thread local's value
     * @param supplier the supplier to be used to determine the initial value
     * @return a new thread local variable
     * @throws NullPointerException if the specified supplier is null
     * @since 1.8
     */
    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }

    /**
     * Creates a thread local variable.
     * @see #withInitial(java.util.function.Supplier)
     */
    public ThreadLocal() {
    }

    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    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();
    }

    /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

    /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    /**
     * Removes the current thread's value for this thread-local
     * variable.  If this thread-local variable is subsequently
     * {@linkplain #get read} by the current thread, its value will be
     * reinitialized by invoking its {@link #initialValue} method,
     * unless its value is {@linkplain #set set} by the current thread
     * in the interim.  This may result in multiple invocations of the
     * {@code initialValue} method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
  1. get()方法用於獲取當前線程的副本變量值。
  2. set()方法用於保存當前線程的副本變量值。
  3. initialValue()爲當前線程初始副本變量值。
  4. remove()方法移除當前線程的副本變量值。

4、 ThreadLocal業務場景能幹嘛?

spring中@Transacion註解中使用到。
Spring的事務管理器通過AOP切入業務代碼,在進入業務代碼前,會依據相應的事務管理器提取出相應的事務對象,假如事務管理器是DataSourceTransactionManager,就會從DataSource中獲取一個連接對象,通過一定的包裝後將其保存在ThreadLocal中。而且Spring也將DataSource進行了包裝,重寫了當中的getConnection()方法,或者說該方法的返回將由Spring來控制,這樣Spring就能讓線程內多次獲取到的Connection對象是同一個。事務操作時,如果插入時出現了異常,依然可以拿到原來的連接進行回滾操作。

爲什麼要放在ThreadLocal裏面呢?由於Spring在AOP後並不能嚮應用程序傳遞參數。應用程序的每一個業務代碼是事先定義好的,Spring並不會要求在業務代碼的入口參數中必須編寫Connection的入口參數。此時Spring選擇了ThreadLocal,通過它保證連接對象始終在線程內部,不論什麼時候都能拿到,此時Spring很清楚什麼時候回收這個連接,也就是很清楚什麼時候從ThreadLocal中刪除這個元素(在5.2節中會具體解說)。

5、ThreadLocalMap的問題

面試相關問題:

5.1爲什麼ThreadLocalMap 中的key是弱引用? 內存泄漏的第一種場景

ThreadLocal爲什麼要用弱引用
上圖中,若是用強引用,即使t1=null,但key的引用仍然指向ThreadLocal對象,GC時只會把t1給回收掉,ThreadLocal由於被引用了不會被回收,所以使用強引用了之後會導致內存泄漏。

5.2 爲什麼threadLocal用完必須要進行remove? 內存泄漏的第二種場景

由於ThreadLocalMap的**key是弱引用,而Value是強引用。**這就導致了一個問題,ThreadLocal在沒有外部對象強引用時,發生GC時弱引用Key會被回收,而Value不會回收,如果創建ThreadLocal的線程一直持續運行,那麼這個Entry對象中的value就有可能一直得不到回收,發生內存泄露。
既然Key是弱引用,那麼我們要做的事,就是在調用ThreadLocal的get()、set()方法時完成後再調用remove方法,將Entry節點和Map的引用關係移除,這樣整個Entry對象在GC Roots分析後就變成不可達了,下次GC的時候就可以被回收。
如果使用ThreadLocal的set方法之後,沒有顯示的調用remove方法,就有可能發生內存泄露,所以養成良好的編程習慣十分重要,使用完ThreadLocal之後,記得調用remove方法

ThreadLocal<Person> threadLocal = new ThreadLocal<Person>();
try {
     threadLocal.set(new Person());
} finally {
     threadLocal.remove();   //threadlocal用完必須要進行remove,不然會導致內存泄漏。
}

總結

  • 每個ThreadLocal只能保存一個變量副本,如果想要上線一個線程能夠保存多個副本以上,就需要創建多個ThreadLocal
  • ThreadLocal內部的ThreadLocalMap鍵爲弱引用,會有內存泄漏的風險。
  • 適用於無狀態,副本變量獨立後不影響業務邏輯的高併發場景。如果如果業務邏輯強依賴於副本變量,則不適合用ThreadLocal解決,需要另尋解決方案。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章