ThreadLocal原理及使用

ThreadLocal 兩大使用場景

文章首發於公衆號,歡迎訂閱
在這裏插入圖片描述

場景一:每個線程需要一個獨享的對象(通常是工具類,典型需要使用的類有 SimpleDateFormatRandom)。

public class ThreadLocalDemo1 {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int end = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalDemo1().date(end);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
        return dateFormat.format(date);
    }
}

class ThreadSafeFormatter {

    // 寫法一
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    // 寫法二
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
            .withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}

場景二:每個線程內需要保存類似於全局變量的信息(例如在攔截器中獲取用戶信息,該信息在本線程執行的各方法中保持不變),可以讓不同方法直接使用,卻不想被多線程共享(因爲不同線程獲取到的用戶信息不一樣),避免參數傳遞的麻煩。

public class ThreadLocalDemo2 {

    public static void main(String[] args) {
        new Service1().process("");

    }
}

class Service1 {

    public void process(String name) {
        User user = new User("feichaoyu");
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}

class Service2 {

    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service2拿到用戶名:" + user.name);
        new Service3().process();
    }
}

class Service3 {

    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service3拿到用戶名:" + user.name);
        UserContextHolder.holder.remove();
    }
}

class UserContextHolder {

    public static ThreadLocal<User> holder = new ThreadLocal<>();
}

class User {

    String name;

    public User(String name) {
        this.name = name;
    }
}

可以得出 ThreadLocal 的兩大作用:

  • 讓某個需要用到的對象在線程間隔離(每個線程都有自己的獨立的對象)。
  • 在任何方法中都可以輕鬆獲取到該對象。

使用 ThreadLocal 的好處

  • 達到線程安全
  • 不需要加鎖,提高執行效率。
  • 更高效地利用內存、節省開銷:相比於每個任務都新建一個 SimpleDateFormat,顯然用 ThreadLocal 可以節省內存和開銷。
  • 免去傳參的繁瑣:無論是場景一的工具類,還是場景二的用戶名,都可以在任何地方直接通過 ThreadLocal 拿到,再也不需要每次都傳同樣的參數。Threadlocal 使得代碼耦合度更低,更優雅。

ThreadLocal 的實現原理

概述

Thread 類中有一個 threadLocals 和一個 inheritableThreadLocals , 它們都是 ThreadLocalMap 類型的變量 。在默認情況下, 每個線程中的這兩個變量都爲 null,只有當前線程第一次調用 ThreadLocalset 或者 get 方法時纔會創建它們 。 其實每個線程的本地變量不是存放在 ThreadLocal 實例裏面,而是存放在調用線程的 threadLocals 變量裏面 。 也就是說 , ThreadLocal 類型的本地變量存放在具體的線程內存空間中 。 ThreadLocal 就是一個工具殼,它通過 set 方法把 value 值放入調用線程的 threadLocals 裏面並存放起來 , 當調用線程調用它的 get 方法時,再從當前線程的 threadLocals 變量裏面將其拿出來使用 。 如果調用線程一直不終止, 那麼這個本地變量會一直存放在調用線程的 threadLocals 變量裏面 ,所以當不需要使用本地變量時可以通過調用 ThreadLocal 變量的 remove 方法 ,從當前線程的 threadLocals 裏面刪除該本地變量 。

get 方法源碼

get 方法:

public T get() {
    // 獲取當前線程t
    Thread t = Thread.currentThread();
    // 獲取當前線程t持有的map
    ThreadLocalMap map = getMap(t);

    // 如果map不爲null,返回其鍵值對中保存的value
    if (map != null) {
        // this指的是當前ThreadLocal,通過當前ThreadLocal獲取Entry
        ThreadLocalMap.Entry e = map.getEntry(this);

        // 如果Entry存在,通過Entry獲取值
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }

    // 1.map爲空
    // 2.map不爲空,但是還沒有存儲當前的ThreadLocalMap對象
    // 則執行以下邏輯
    return setInitialValue();
}

setInitialValue 方法:

private T setInitialValue() {
    // 可以自己設置初值,否則默認爲null
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // 如果map不爲空,則更新值
    if (map != null)
        map.set(this, value);
    // 否則創建新的map
    else
        createMap(t, value);
    return value;
}

createMap 方法:

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

set 方法源碼

set 方法:

public void set(T value) {
    // 獲取當前線程t
    Thread t = Thread.currentThread();
    // 獲取當前線程t持有的map
    ThreadLocalMap map = getMap(t);
    // 如果map不爲空,則更新值
    if (map != null)
        map.set(this, value);
    // 否則創建新的map
    else
        createMap(t, value);
}

remove 方法源碼

remove 方法:

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

remove(this)方法:

private void remove(ThreadLocal<?> key) {
    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)]) {
        // 找到要刪除的key
        if (e.get() == key) {
            // 去除key的軟引用
            e.clear();
            // 
            expungeStaleEntry(i);
            return;
        }
    }
}

expungeStaleEntry 方法:

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        // 注意到這裏
        ThreadLocal<?> k = e.get();
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

expungeStaleEntry方法調用前有這麼一行代碼e.clear();,該行代碼去除了 key 的軟引用,那麼在expungeStaleEntry方法中我們注意到ThreadLocal<?> k = e.get();,此時 knull,符合下面的 if 判斷,會將e.value置爲 null,完成了整個鍵值對的釋放,此時就不會內存泄漏了。

ThreadLocal 原理總結

  1. 每個 Thread 維護着一個 ThreadLocalMap 的引用
  2. ThreadLocalMap 是 ThreadLocal 的內部類,用 Entry 來進行存儲
  3. 調用 ThreadLocal 的 set() 方法時,實際上就是往 ThreadLocalMap 設置值,key 是 ThreadLocal 對象,值是傳遞進來的對象
  4. 調用 ThreadLocal 的 get() 方法時,實際上就是往 ThreadLocalMap 獲取值,key 是 ThreadLocal 對象
  5. ThreadLocal 本身並不存儲值,它只是作爲一個 key 來讓線程從 ThreadLocalMap 獲取 value。

ThreadLocal 的內存泄漏

內存泄漏是指,某個對象不再使用,但是佔用的內存無法回收。

ThreadLocalMap 中有一個 Entry 內部類,它的代碼如下:

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

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

Entry 中保存了鍵和值,鍵使用的是 WeakReference 弱引用(可以自動被 GC 回收),值使用的是強引用(無法自動被 GC 回收)。

正常情況下,當線程停止,保存在 ThreadLocal 中的 value 會被垃圾回收,因爲沒有任何強引用了。但是如果線程不停止,那麼 value 就無法被垃圾回收。

鏈路:Thread -> ThreadLocalMap -> Entry -> Value

因爲 valueThread 之間還存在這個強引用鏈路,所以導致 value 無法回收,就可能會出現 OOM。

因此阿里規約中指出,在使用完 ThreadLocal 後,應該調用 remove 方法。

原理是在 remove 方法中,會調用 expungeStaleEntry 方法,這個方法的意思是清除老的Entry

if (k == null) {
    e.value = null;
}

我創建了一個免費的知識星球,用於分享知識日記,歡迎加入!

在這裏插入圖片描述

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