首頁>技術>

一、學習目標

1、ThreadLocal能解決什麼問題?

2、ThreadLocal相比synchronized、volatile的優勢在哪裡?

synchronized:在併發量小的情況下還好,如果併發量較大時,會有大量的執行緒等待同一個物件鎖,會造成系統吞吐量直線下降。volatile:修飾的變數不保留複製,直接訪問主記憶體,主要用於一寫多讀的場景ThreadLocal:給每一個執行緒都建立變數的副本,保證每個執行緒訪問都是自己的副本,相互隔離,就不會出現執行緒安全問題二、ThreadLocal的使用

1、執行緒不安全:

public class ThreadA extends Thread {	private int i;	private UnsafeThread unsafeThread;	ThreadA(int i, UnsafeThread unsafeThread) {		this.i = i;		this.unsafeThread = unsafeThread;	}	@Override	public void run() {		try {			Thread.sleep(10);		} catch (InterruptedException e) {			e.printStackTrace();		}		unsafeThread.calc();		System.out.println("i:" + i + ",count:" + unsafeThread.getCount());	}}public class UnsafeThread {	private int count = 0;	public void calc() {		count++;	}	public int getCount() {		return count;	}	public static void main(String[] args) throws InterruptedException {		UnsafeThread testThread = new UnsafeThread();		for (int i = 0; i < 20; i++) {			new ThreadA(i, testThread).start();		}		Thread.sleep(200);		System.out.println("realCount:" + testThread.getCount());	}}

執行結果:

i:6,count:8i:0,count:8i:17,count:13i:7,count:8i:2,count:8i:11,count:8i:8,count:11i:5,count:11i:13,count:11i:15,count:11i:14,count:11i:10,count:8i:3,count:11i:12,count:11i:9,count:8i:19,count:15i:4,count:15i:16,count:15i:1,count:15i:18,count:15realCount:15

我們可以看到:出現了執行緒安全問題

realCount最終出現錯誤,預計的結果應該是20,實際情況卻是15count出現重複

2、加入ThreadLocal,執行緒安全:

public class ThreadA extends Thread {	private int i;	private UnsafeThread unsafeThread;	ThreadA(int i, UnsafeThread unsafeThread) {		this.i = i;		this.unsafeThread = unsafeThread;	}	@Override	public void run() {		try {			Thread.sleep(10);		} catch (InterruptedException e) {			e.printStackTrace();		}		unsafeThread.calc();		System.out.println("i:" + i + ",count:" + unsafeThread.getCount());	}}public class SafeThread {	private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();	private int count = 0;	public void calc() {		threadLocal.set(count + 1);	}	public int getCount() {		Integer integer = threadLocal.get();		return integer != null ? integer : 0;	}	public static void main(String[] args) throws InterruptedException {		SafeThread testThreadLocal = new SafeThread();		for (int i = 0; i < 20; i++) {			new ThreadB(i, testThreadLocal).start();		}		Thread.sleep(200);		System.out.println("realCount:" + testThreadLocal.getCount());	}}

執行結果:

i:1,count:1i:10,count:1i:4,count:1i:11,count:1i:6,count:1i:7,count:1i:9,count:1i:3,count:1i:12,count:1i:0,count:1i:5,count:1i:8,count:1i:2,count:1i:13,count:1i:17,count:1i:18,count:1i:15,count:1i:19,count:1i:16,count:1i:14,count:1realCount:0
三、ThreadLocal的工作原理

1、執行緒不安全:

我們可以看到多個執行緒可以同時訪問公共資源count,當某個執行緒在執行count++的時候,可能其他的執行緒正好同時也執行count++。但由於多個執行緒變數count的不可見性,會導致另外的執行緒拿到舊的count值+1,這樣就出現了realCount預計是20,但是實際上是15的資料問題。

2、執行緒安全:

如圖所示:

往大的方向上說,ThreadLocal會給每一個執行緒都建立變數的副本,保證每個執行緒訪問都是自己的副本,相互隔離。往小的方向上說, 每個執行緒內部都有一個threadLocalMap,每個threadLocalMap裡面都包含了一個entry陣列,而entry是由threadLocal和資料(這裡指的是count)組成的。

這樣一來,每個執行緒都擁有自己專屬的變數count。

示例2中,執行緒1呼叫calc方法時,會先呼叫的getCount方法,由於第一次呼叫threadLocal.get()返回是空的,所以getCount返回值是0。這樣threadLocal.set(getCount() + 1),就變成了threadLocal.set(0 + 1),它會給執行緒1中threadLocal的資料值設定成1。

執行緒2再呼叫calc方法,同樣會先呼叫getCount方法,由於第一次呼叫threadLocal.get()返回是空的,所以getCount返回值也是0。這樣threadLocal.set(getCount() + 1),會給執行緒2中threadLocal的資料值也設定成1。

......

最後每個執行緒的threadLocal中的資料值都是1。

還有,示例2中打印出來的realCount為什麼是0呢?

因為testThreadLocal.getCount()是在主執行緒中呼叫的,其他的執行緒改變只會影響自己的副本,不會影響原始變數,count初始值是0,所以最後還是0。四、ThreadLocal的原始碼解析

1、Thread:

ThreadLocal.ThreadLocalMap threadLocals = null;

定義了一個叫threadLocals的成員變數,它的型別是ThreadLocal.ThreadLocalMap。很明顯ThreadLocalMap是ThreadLocal的內部類,驗證了我在圖中畫的內容, 每個執行緒都有一個ThreadLocalMap物件。

2、ThreadLocalMap:

static class ThreadLocalMap {        // Entry是WeakReference(弱引用)的子類    static class Entry extends WeakReference<ThreadLocal<?>> {        Object value;                // Entry 包含了 ThreadLocal變數 和Object的value        Entry(ThreadLocal<?> k, Object v) {            // ThreadLocal變數做為WeakReference的referen            super(k);            value = v;        }    }    private static final int INITIAL_CAPACITY = 16;        // 陣列,它的型別是Entry    private Entry[] table;    private int size = 0;    private int threshold; // Default to 0    private void setThreshold(int len) {        threshold = len * 2 / 3;    }        ...}    
3、ThreadLocal#get():
public T get() {    //獲取當前執行緒    Thread t = Thread.currentThread();    //獲取當前執行緒中的ThreadLocalMap物件    ThreadLocalMap map = getMap(t);        //如果可以查詢到資料    if (map != null) {        //從ThreadLocalMap中獲取entry物件        ThreadLocalMap.Entry e = map.getEntry(this);        //如果entry存在        if (e != null) {            @SuppressWarnings("unchecked")            //獲取entry中的值            T result = (T)e.value;            //返回獲取到的值            return result;        }    }        //呼叫初始化方法,返回null    return setInitialValue();}

ThreadLocal#getMap(t):

ThreadLocalMap getMap(Thread t) {    return t.threadLocals;}

實際上是呼叫Thread類的變數:

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal#getEntry(ThreadLocal<?> key):

private Entry getEntry(ThreadLocal<?> key) {    //threadLocalHashCode是key的hash值    //key.threadLocalHashCode & (table.length - 1),    //相當於threadLocalHashCode對table.length - 1的取餘操作,    //這樣可以保證陣列的下表在0到table.length - 1之間。    int i = key.threadLocalHashCode & (table.length - 1);    //獲取下標對應的entry    Entry e = table[i];    //如果entry不為空,並且從弱引用中獲取到的值(threadLocal) 和 key相同     if (e != null && e.get() == key)        //返回獲取到的entry        return e;    else       //如果沒有獲取到entry或者e.get()獲取不到資料,則清理空資料        return getEntryAfterMiss(key, i, e);}

entry是WeakReference的子類,那麼e.get()方法會呼叫:Refernt#get()

public T get() {  return this.referent;}
返回的是一個引用,這個引用就是構造器傳入的threadLocal物件。

ThreadLocal#getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {    Entry[] tab = table;    int len = tab.length;    while (e != null) {        ThreadLocal<?> k = e.get();        if (k == key)            return e;        if (k == null)            expungeStaleEntry(i);        else            i = nextIndex(i, len);        e = tab[i];    }    return null;}
該方法裡面會呼叫expungeStaleEntry方法,後面我們會重點介紹的

ThreadLocal#setInitialValue():

private T setInitialValue() {    //呼叫使用者自定義的initialValue方法,預設值是null    T value = initialValue();    //獲取當前執行緒    Thread t = Thread.currentThread();    //獲取當前執行緒中的ThreadLocalMap,跟之前一樣    ThreadLocalMap map = getMap(t);    //如果ThreadLocalMap不為空,    if (map != null)        //則覆蓋key為當前threadLocal的值        map.set(this, value);    else       //否則建立新的ThreadLocalMap        createMap(t, value);    //返回使用者自定義的值        return value;}
4、ThreadLocal#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.ThreadLocalMap#set(ThreadLocal<?> key, Object value):

private void set(ThreadLocal<?> key, Object value) {    //將table陣列賦值給新陣列tab    Entry[] tab = table;    //獲取陣列長度    int len = tab.length;    //跟之前一樣計算陣列中的下表    int i = key.threadLocalHashCode & (len-1);    //迴圈變數tab獲取entry    for (Entry e = tab[i];         e != null;         e = tab[i = nextIndex(i, len)]) {        //獲取entry中的threadLocal物件         ThreadLocal<?> k = e.get();        //如果threadLocal物件不為空,並且等於key        if (k == key) {            //覆蓋已有資料            e.value = value;            //返回            return;        }        //如果threadLocal物件為空        if (k == null) {            //建立一個新的entry賦值給已有key            replaceStaleEntry(key, value, i);            return;        }    }    //如果key不在已有資料中,則建立一個新的entry    tab[i] = new Entry(key, value);    //長度+1    int sz = ++size;    if (!cleanSomeSlots(i, sz) && sz >= threshold)        rehash();}
replaceStaleEntry方法也會呼叫expungeStaleEntry方法。5、ThreadLocal#remove():
public void remove() { //還是那個套路,不過簡化了一下 //先獲取當前執行緒,再獲取執行緒中的ThreadLocalMap物件 ThreadLocalMap m = getMap(Thread.currentThread()); //如果ThreadLocalMap不為空 if (m != null)     //刪除資料     m.remove(this);}

ThreadLocal.ThreadLocalMap#remove(ThreadLocal<?> key):

private void remove(ThreadLocal<?> key) {    //將table陣列賦值給新陣列tab    Entry[] tab = table;    //獲取陣列長度    int len = tab.length;    //跟之前一樣計算陣列中的下表    int i = key.threadLocalHashCode & (len-1);    //迴圈變數從下表i之後不為空的entry    for (Entry e = tab[i];         e != null;         e = tab[i = nextIndex(i, len)]) {        //如果可以獲取到threadLocal並且值等於key         if (e.get() == key) {            //清空引用            e.clear();            //處理threadLocal為空但是value不為空的entry            expungeStaleEntry(i);            return;        }    }}

其中的clear方法,也很簡單,只是把引用設定為null,即清空引用

public void clear() {    this.referent = null;}
ThreadLocal#expungeStaleEntry(int staleSlot)
private int expungeStaleEntry(int staleSlot) {    Entry[] tab = table;    int len = tab.length;    //將位置staleSlot對應的entry中的value設定為null,有助於垃圾回收    tab[staleSlot].value = null;    //將位置staleSlot對應的entry設定為null,有助於垃圾回收    tab[staleSlot] = null;    //陣列大小-1    size--;    Entry e;    int i;    //變數staleSlot之後entry不為空的資料    for (i = nextIndex(staleSlot, len);         (e = tab[i]) != null;         i = nextIndex(i, len)) {        //獲取當前位置的entry中對應的threadLocal         ThreadLocal<?> k = e.get();        //threadLocal為空,說明是髒資料        if (k == null) {            //value設定為null,有助於垃圾回收            e.value = null;            //當前位置的entry設定為null            tab[i] = null;            //陣列大小-1            size--;        } else {            //重新計算位置            int h = k.threadLocalHashCode & (len - 1);            //如果h和i不相等,說明存在hash衝突            //現在它前面的髒Entry被清理            //該Entry需要向前移動,防止下次get()或set()的時候            //再次因雜湊衝突而查詢到null值            if (h != i) {                tab[i] = null;                while (tab[h] != null)                    h = nextIndex(h, len);                tab[h] = e;            }        }    }    return i;}

該方法首先清除當前位置的髒Entry,然後向後遍歷直到table[i]==null。在遍歷的過程中如果再次遇到髒Entry就會清理。

如果沒有遇到就會重新變數當前遇到的Entry,如果重新雜湊得到的下標h與當前下標i不一致,說明該Entry被放入Entry陣列的時候發生了雜湊衝突(其位置透過再雜湊被向後偏移了),現在其前面的髒Entry已經被清除,所以當前Entry應該向前移動,補上空位置。否則下次呼叫set()或get()方法查詢該Entry的時候會查詢到位於其之前的null值。

為什麼要做這樣的清除?

我們知道entry物件裡面包含了threadLocal和value,threadLocal是WeakReference(弱引用)的referent。每次垃圾回收期觸發GC的時候,都會回收WeakReference的referent,會將referent設定為null。那麼table陣列中就會存在很多threadLocal = null 但是 value不為空的entry,這種entry的存在是沒有任何實際價值的。這種資料透過getEntry是獲取不到值,因為它裡面有if (e != null && e.get() == key)這句判斷。

為什麼要使用WeakReference(弱引用)?

避免記憶體洩漏:如果使用強引用,ThreadLocal在使用者程序不再被引用,但是隻要執行緒不結束,在ThreadLocalMap中就還存在引用,無法被GC回收,會導致記憶體洩漏。另外在使用執行緒池技術的時候,由於執行緒不會被銷燬,回收之後,下一次又會被重複利用,會導致ThreadLocal無法被釋放,最終也會導致記憶體洩露問題。四、ThreadLocal有哪些坑1、記憶體洩露問題:

ThreadLocal即使使用了WeakReference(弱引用)也可能會存在記憶體洩露問題,因為 entry物件中只把key(即threadLocal物件)設定成了弱引用,但是value值沒有。還是會存在下面的強依賴:

Thread -> ThreaLocalMap -> Entry -> value

解決方案:

呼叫get()、set(T value) , 但是 get()和set(T value) 方法是基於垃圾回收器把key回收之後的基礎之上觸發的資料清理。如果出現垃圾回收器回收不及時的情況,也一樣有問題。呼叫remove(),該方法會把entry中的key(即threadLocal物件)和value一起清空。2、執行緒安全問題:

可能有些朋友認為使用了threadLocal就不會出現執行緒安全問題了,其實是不對的。 假如我們定義了一個static的變數count,多執行緒的情況下,threadLocal中的value需要修改並設定count的值,它一樣有問題。因為static的變數是多個執行緒共享的,不會再單獨儲存副本。

五、總結:

1、每個執行緒都有一個threadLocalMap物件,每個threadLocalMap裡面都包含了一個entry陣列,而entry是由key(即threadLocal)和value(資料)組成。

2、entry的key是弱引用,可以被垃圾回收器回收。

3、threadLocal最常用的這四個方法:get(), initialValue(),set(T value) 和 remove(),除了initialValue方法,其他的方法都會呼叫expungeStaleEntry方法做key==null的資料清理工作,以便於垃圾回收。

4、呼叫get()、set(T value) , 但是 get()和set(T value) 方法是基於垃圾回收器把key回收之後的基礎之上觸發的資料清理。如果出現垃圾回收器回收不及時的情況,也一樣有問題記憶體洩漏問題,最保險是在使用完threadLocal之後,手動呼叫一下remove方法,從原始碼可以看到,該方法會把entry中的key(即threadLocal物件)和value一起清空。

14
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • Lombok的介紹及安裝