一、學習目標
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一起清空。