ThreadLocal用法及原理

蘇格團隊發表於2019-03-22
  • 蘇格團隊
  • 作者:ZhiCheng

與Synchonized的對照: ThreadLocal和Synchonized都用於解決多執行緒併發訪問。可是ThreadLocal與synchronized有本質的差別。synchronized是利用鎖的機制,使變數或程式碼塊在某一時該僅僅能被一個執行緒訪問。而ThreadLocal為每個執行緒都提供了變數的副本,使得每個執行緒在某一時間訪問到的並非同一個物件,這樣就隔離了多個執行緒對資料的資料共享。而Synchronized卻正好相反,它用於在多個執行緒間通訊時可以獲得資料共享。

Synchronized用於執行緒間的資料共享,而ThreadLocal則用於執行緒間的資料隔離。


一、用法

把要執行緒隔離的資料放進ThreadLocal

static ThreadLocal<T> threadLocal = new ThreadLocal<T>() {
	protected T initialValue() {
		這裡一般new一個物件返回
    }
}
複製程式碼

執行緒獲取相關資料的時候只要

threadLocal.get();
複製程式碼

想修改、賦值只要

threadLocal.set(val)
複製程式碼

二 、使用場景

如上面說到的,ThreadLocal是用於執行緒間的資料隔離,ThreadLocal為每個執行緒都提供了變數的副本。

  • 舉例1:聯想一下伺服器(例如tomcat)處理請求的時候,會從執行緒池中取一條出來進行處理請求,如果想把每個請求的使用者資訊儲存到一個靜態變數裡以便在處理請求過程中隨時獲取到使用者資訊。這時候可以建一個攔截器,請求到來時,把使用者資訊存到一個靜態ThreadLocal變數中,那麼在請求處理過程中可以隨時從靜態ThreadLocal變數獲取使用者資訊。

  • 舉例2:Spring的事務實現也藉助了ThreadLocal類。Spring會從資料庫連線池中獲得一個connection,然會把connection放進ThreadLocal中,也就和執行緒繫結了,事務需要提交或者回滾,只要從ThreadLocal中拿到connection進行操作。

三、原理分析

1、get()方法

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {//當map已存在
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();//初始化值
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
複製程式碼

上面先取到當前執行緒,然後呼叫getMap方法獲取對應的ThreadLocalMap,ThreadLocalMap是ThreadLocal的靜態內部類,然後Thread類中有一個這樣型別成員,所以getMap是直接返回Thread的成員

ThreadLocal.ThreadLocalMap threadLocals = null;
複製程式碼

來看下ThreadLocal的內部類ThreadLocalMap原始碼,留個大致印象

static class ThreadLocalMap {
	private static final int INITIAL_CAPACITY = 16;//初始陣列大小
    private Entry[] table;//每個可以擁有多個ThreadLocal
    private int size = 0;
    private int threshold;//擴容閥值
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
 
    private Entry getEntry(ThreadLocal<?> key) {
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        if (e != null && e.get() == key)
            return e;
        else
            return getEntryAfterMiss(key, i, e);
    }
    
    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) {
            		//迴圈利用key過期的Entry
                replaceStaleEntry(key, value, i);
                return;
            }
        }
        tab[i] = new Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }
	...
}
複製程式碼

可以看到有個Entry內部靜態類,它繼承了WeakReference,總之它記錄了兩個資訊,一個是ThreadLocal<?>型別,一個是Object型別的值。getEntry方法則是獲取某個ThreadLocal對應的值,set方法就是更新或賦值相應的ThreadLocal對應的值。裡面涉及到擴容策略、Entry雜湊衝突、迴圈利用等等不再深入,留個大致印象就好。

回顧下get()方法中的程式碼

if (map != null) {
    ThreadLocalMap.Entry e = map.getEntry(this);
    if (e != null) {
        @SuppressWarnings("unchecked")
        T result = (T)e.value;
        return result;
    }
}
return setInitialValue();
複製程式碼

map為null或e為null就會走到setInitialValue,如果我們是第一次get()方法,那map會是空的,所以接下來先看setInitialValue()方法

private T setInitialValue() {
		//呼叫我們實現的方法得到需要執行緒隔離的值
    T value = initialValue();
    Thread t = Thread.currentThread();
    //拿到相應執行緒的ThreadLocalMap成員變數
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}
複製程式碼

上面initialValue就是例項化ThreadLocal要實現的方法,這裡又取了執行緒的ThreadLocalMap,不為空就把值set進去(鍵為TreadLocal本身,值就是initialValue返回的值);為空就建立一個map同時新增一個值進去,最後返回value。

map.set(this, value)這句程式碼在上面的ThreadLocalMap原始碼中可以看到大致流程,下面看看createMap()做了什麼事

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


ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    	//建立一個Entry,加入陣列
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}
複製程式碼

可以看到在new ThreadLocalMap之後,就會建立一個Entry加入到陣列中,最後把ThreadLocalMap的引用賦值給Thread的threadLocals成員變數

在回顧下get()方法中的程式碼

if (map != null) {
    ThreadLocalMap.Entry e = map.getEntry(this);
    if (e != null) {
        @SuppressWarnings("unchecked")
        T result = (T)e.value;
        return result;
    }
}
return setInitialValue();
複製程式碼

現在map不會為空了,再次呼叫get方法就會呼叫map的getEntry方法(上面的ThreadLocalMap原始碼中可以看到大致流程),拿到相應的Entry,然後就可以拿到相應的值返回出去

2、set方法

分析完get()方法,那麼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);
}
複製程式碼

總結

  • 原理

    ThreadLocal的實現原理是,在每個執行緒中維護一個Map,鍵是ThreadLocal型別,值是Object型別。當想獲取ThreadLocal的值時,就從當前執行緒中拿出Map,然後在把ThreadLocal本身作為鍵從Map中拿出值返回。

  • 優缺點

    **優點:**提供執行緒內的區域性變數。每個執行緒都自己管理自己的區域性變數,互不影響

    **缺點:**記憶體洩漏問題。可以看到ThreadLocalMap中的Entry是繼承WeakReference的,其中ThreadLocal是以弱引用形式存在Entry中,如果ThreadLocal在外部沒有被強引用,那麼垃圾回收的時候就會被回收掉,又因為Entry中的value是強引用,就會出現記憶體洩漏。雖然ThreadLocal原始碼中的會對這種情況進行了處理,但還是建議不需要用TreadLocal的時候,手動調remove方法。

相關文章