Java面經(jīng)之ThreadLocal底層原理
點(diǎn)擊藍(lán)字關(guān)注我們,獲取更多面經(jīng)

ThreadLocal底層原理
1 概述
ThreadLocal是java.lang包中的一個(gè)類,用來實(shí)現(xiàn)變量的線程封閉性,即只有當(dāng)前線程可以操作該變量,通過把一個(gè)變量存在當(dāng)前線程的一個(gè)Map容器中來實(shí)現(xiàn)。當(dāng)然,這樣解釋很抽象,對(duì)于一些人來說難以理解,這里我先介紹一個(gè)《java并發(fā)編程實(shí)戰(zhàn)》中描述的jdbc應(yīng)用場景,來讓你知道ThreadLocal到底有什么用。
2 ThreadLocal的意義
我們知道,當(dāng)在普通方法中創(chuàng)建一個(gè)變量類,若沒有特別在方法區(qū)域外留有該類的引用,當(dāng)方法結(jié)束后在其它地方不能夠再使用這個(gè)類。當(dāng)我們?cè)谄渌胤竭€要用到方法中創(chuàng)建的對(duì)象時(shí),我們通常會(huì)用一個(gè)全局變量指向這個(gè)對(duì)象,這樣在整個(gè)項(xiàng)目中都能再訪問這個(gè)對(duì)象了。但想想,在多線程情況下,每個(gè)線程訪問該方法都會(huì)創(chuàng)建一個(gè)全局對(duì)象,在高并發(fā)下那我們豈不是要?jiǎng)?chuàng)建成千上萬個(gè)全局變量來存?若該對(duì)象還帶有每個(gè)線程特有的參數(shù),那就要保證每個(gè)線程在之后能調(diào)用自己的創(chuàng)建的對(duì)象,一般情況下是很難進(jìn)行管理的。而ThreadLocal就做到了既能讓對(duì)象在其它地方被創(chuàng)建線程訪問,也省去了自己管理全局對(duì)象的麻煩。
一般來講,在單線程應(yīng)用程序中可能會(huì)維持一個(gè)全局的數(shù)據(jù)庫連接,并在程序啟動(dòng)時(shí)初始化這個(gè)連接對(duì)象,從而避免在調(diào)用每個(gè)方法(save,get等)時(shí)都要傳遞一個(gè)Connection對(duì)象。由于Jdbc連接對(duì)象不一定是線程安全的,因此,當(dāng)多線程應(yīng)用程序在沒有協(xié)同的情況下使用全局變量時(shí),就不是線程安全的,比如線程一剛獲取全局的Connection,準(zhǔn)備進(jìn)行數(shù)據(jù)庫操作,但線程二卻執(zhí)行了Connection.close()。
這種情況下我們可能會(huì)取消Connection這個(gè)全局變量,在每次要進(jìn)行數(shù)據(jù)庫相關(guān)操作時(shí)直接new一個(gè)Connection對(duì)象進(jìn)行連接,而這又會(huì)導(dǎo)致一個(gè)線程執(zhí)行多次數(shù)據(jù)庫操作時(shí)要new多個(gè)connection對(duì)象,加大系統(tǒng)的負(fù)擔(dān)。這時(shí)就會(huì)想能不能有這樣一種方法,既不讓Connection成為全局變量來保證線程安全,又可以實(shí)現(xiàn)全局Connection帶來的“一次連接”式的便利?ThreadLocal就是做這件事的。
3 簡要應(yīng)用
下面第一行代碼new了一個(gè)靜態(tài)的ThreadLocal<Connection>。以后只要讓每個(gè)線程在進(jìn)行jdbc操作前都執(zhí)行第二行代碼,就會(huì)把一個(gè)創(chuàng)建的Connection對(duì)象存放到當(dāng)前線程的map容器中(ThreadLocalMap,見下文介紹),后面該線程要進(jìn)行數(shù)據(jù)庫操作時(shí)只要執(zhí)行第三行代碼,就能拿到這個(gè)引用進(jìn)行連接,不用每次都new一個(gè)新的。
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();connectionHolder.set(DriverManager.getConnection(DB_URL));connectionHolder.get();
你可能會(huì)問只是一句簡單的connectionHolder.get()代碼,而且ThreadLocal對(duì)象只有一個(gè),那怎么能準(zhǔn)確的拿到當(dāng)前線程中的Connection呢?答案就是:這個(gè)connection是存在當(dāng)前線程(一個(gè)Thread)中的,不是ThreadLocal中。表面上調(diào)用的ThreadLocal.get(),實(shí)際上是在當(dāng)前線程對(duì)象的Map容器中進(jìn)行設(shè)置和查找。不過當(dāng)前線程的map容器中可能會(huì)存多個(gè)ThreadLocal的值,所以Map中的key就是ThreadLoca對(duì)象,值就是connection,來進(jìn)行區(qū)分。還不懂的話見下面的代碼解析,很簡單。
4 源代碼分析
ThreadLocalMap是ThreadLocal類中的靜態(tài)內(nèi)部類,可以看成一個(gè)map容器。Thread類中有一個(gè)全局變量threadLocals 就是ThreadLocalMap類型,它才是ThreadLocal存儲(chǔ)數(shù)據(jù)的真正容器。
public class Thread implements Runnable {/* ThreadLocal values pertaining to this thread. This map is maintained* by the ThreadLocal class. */ThreadLocal.ThreadLocalMap threadLocals = null;}
4.1 ThreadLocalMap底層結(jié)構(gòu)
在看ThreadLocal的底層代碼之前,我們先來看看ThreadLocalMap的操作,這樣有利于接下來更好地理解。
static class ThreadLocalMap {private static final int INITIAL_CAPACITY = 16;private int size = 0;private int threshold;private Entry[] table; //Thread的map容器// 構(gòu)造方法,在第一次調(diào)用ThreadLocal.set()時(shí)會(huì)new一個(gè)ThreadLocalMapThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);}static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}}
可能會(huì)好奇,table中的Entry只有一個(gè)value,并沒有像Hashmap中那樣是鍵值對(duì)啊,為什么叫它map容器呢?其實(shí)Entry是有key的,key就是構(gòu)造Entry時(shí)傳入的ThreadLocal參數(shù),不過指向ThreadLocal的成員變量是從Reference類中繼承的。從Entry的構(gòu)造方法中可看到傳入了一個(gè)ThreadLocal對(duì)象,它就是key,我們追蹤super(k),最后發(fā)現(xiàn)在Entry的父類Reference中,傳給了成員變量referent。至于這個(gè)referent是怎么用到容器的操作上,下面馬上會(huì)看到。
public abstract class Reference<T> {private T referent; // key。注入了傳入的ThreadLocal,被Entry繼承//ThreadLocal最終傳給了參數(shù)referentReference(T referent, ReferenceQueue<? super T> queue) {this.referent = referent;this.queue = (queue == null) ? ReferenceQueue.NULL : queue;}// 獲取“key”public T get() {return this.referent;}}
4.2 ThreadLocalMap的set方法
set方法有點(diǎn)像HashMap的put方法,HashMap中也是用一個(gè)Node[] table來存儲(chǔ)數(shù)據(jù),Node里面包含了鍵值對(duì),而這里Entry也是<referent,value>鍵值對(duì)。
首先根據(jù)根據(jù)key的hashcode計(jì)算出插入的Entry在table中的位置,然后從計(jì)算出的位置向前循環(huán)遍歷,直到找到的Entry為null或key為null(被GC了),就把Entry插入到該位置。在遍歷的過程中會(huì)判斷遍歷元素與插入元素的是否是相同的Entry,規(guī)則是比較Entry的key即referent??梢钥吹较旅嫱ㄟ^Entry.get()來獲取key,而get()方法繼承自Reference父類中,就是返回referent屬性。
private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;int len = tab.length;//根據(jù)ThreadLocal的hash值計(jì)算應(yīng)該從table中哪個(gè)位置開始(跟HashMap一樣)int i = key.threadLocalHashCode & (len-1);//遍歷table中的每一個(gè)Entryfor (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();//如果找到相同的key,覆蓋并返回if (k == key) {e.value = value;return;}//如果發(fā)現(xiàn)了空key為空,即ThreadLocal對(duì)象被GC了,就把數(shù)據(jù)放到該位置if (k == null) {replaceStaleEntry(key, value, i);return;}}//當(dāng)返現(xiàn)e==null時(shí),執(zhí)行下面代碼,new再檢測(cè)是否擴(kuò)容tab[i] = new Entry(key, value);int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}
4.3 ThreadLocalMap的getEntry方法
首先根據(jù)key找到table中的一個(gè)位置,如果該位置上的元素不為空且key等于傳入的key,則直接返回該位置上的Entry;若位置上的元素為null或key為null,則返回null;否則向前循環(huán)遍歷table直到發(fā)現(xiàn)key相等的Entry返回,或遍歷到null直接返回null。
private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)return e;elsereturn getEntryAfterMiss(key, i, 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);elsei = nextIndex(i, len);e = tab[i];}return null;}
4.4 ThreadLocal的set方法
先拿到當(dāng)前線程的ThreadLocalMap容器,若線程的容器為null(一次都沒有使用過)則初始化,創(chuàng)建一個(gè)新的ThreadLocalMap同時(shí)將key和value傳入,插入到指定位置,ThreadLocalMap的構(gòu)造方法參見4.1中的代碼。若不為空則直接調(diào)用該容器的set方法插入鍵值對(duì)。兩種方法都是把自己當(dāng)key傳入。
public void set(T value) {Thread t = Thread.currentThread(); //獲取當(dāng)前線程對(duì)象ThreadLocalMap map = getMap(t); //拿到t的map容器if (map != null)map.set(this, value); //map不空,根據(jù)key(調(diào)用set方法的ThreadLocal對(duì)象)設(shè)置值elsecreateMap(t, value); //map為空則創(chuàng)建一個(gè)map(第一次插入,有點(diǎn)怪怪的)}// 從當(dāng)前線程中獲取ThreadLocalMapThreadLocalMap getMap(Thread t) {return t.threadLocals;}// 初始化當(dāng)前線程的ThreadLocalMap容器void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}
4.5 ThreadLocal的get方法
很簡單,直接調(diào)用的ThreadLocalMap的get方法,把自己作為key傳入getEntry方法中。若容器為null,則調(diào)用setInitialValue方法。
public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {T result = (T)e.value;return result;}}return setInitialValue();}
4.6ThreadLocal的setInitialValue方法
會(huì)返回一個(gè)初始值,由initialValue返回??梢钥吹絠nitialValue方法只是返回了一個(gè)null,如果我們不重寫它的話。同時(shí)再加一個(gè)判斷,若容器為null,則跟set方法一樣進(jìn)行初始化,不過這里的value不是傳入的,而是initialValue,默認(rèn)為null。
private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value;}protected T initialValue() {return null;}
5 案例理解
下面創(chuàng)建了5個(gè)線程,在每個(gè)線程執(zhí)行期間調(diào)用了兩個(gè)ThreadLocal對(duì)象的set方法,然后打印get方法的返回值。顯然同一個(gè)ThreadLocal在不同線程中調(diào)用get和set方法是能區(qū)分線程的。
public class Test1 {public static void main(String[] args) {for (int i=0;i<5;++i){new MyThread().start();}}}class MyThread extends Thread{private static final ThreadLocal<String> threadLocal1 = new ThreadLocal<String>();private static final ThreadLocal<String> threadLocal2 = new ThreadLocal<String>();@Overridepublic void run(){threadLocal1.set(getName()+":調(diào)用了threadLocal1的set方法");threadLocal2.set(getName()+":調(diào)用了threadLocal2的set方法");System.out.println(threadLocal1.get());System.out.println(threadLocal2.get());}}/*---------------打印結(jié)果-----------------Thread-0:調(diào)用了threadLocal1的set方法Thread-0:調(diào)用了threadLocal2的set方法Thread-2:調(diào)用了threadLocal1的set方法Thread-2:調(diào)用了threadLocal2的set方法Thread-3:調(diào)用了threadLocal1的set方法Thread-4:調(diào)用了threadLocal1的set方法Thread-3:調(diào)用了threadLocal2的set方法Thread-4:調(diào)用了threadLocal2的set方法Thread-1:調(diào)用了threadLocal1的set方法Thread-1:調(diào)用了threadLocal2的set方法*/
5 注意:臟讀、內(nèi)存泄漏
5.1 臟讀
當(dāng)使用線程池的時(shí)候,由于工作線程是循環(huán)利用的,上一個(gè)任務(wù)線程通過ThreadLocal在工作線程中存入了數(shù)據(jù),下一個(gè)任務(wù)線程被該工作線程執(zhí)行時(shí),依然能夠讀到上一個(gè)任務(wù)線程存入的數(shù)據(jù),也就是讀到了臟數(shù)據(jù)。
解決辦法就是在一個(gè)線程執(zhí)行結(jié)束后將數(shù)據(jù)通過ThreadLocal.remove()刪除掉。
5.2 內(nèi)存泄漏
由于ThreadLocalMap是以弱引用的方式引用著ThreadLocal,換句話說,就是ThreadLocal是被ThreadLocalMap以弱引用的方式關(guān)聯(lián)著,因此如果ThreadLocal沒有被ThreadLocalMap以外的對(duì)象引用(如手動(dòng)令ThreadLocal = null或下面紅色字體),則在下一次GC的時(shí)候,ThreadLocal實(shí)例就會(huì)被回收,那么此時(shí)ThreadLocalMap里的一組KV的K就是null了,因此在沒有額外操作的情況下,此處的V便不會(huì)被外部訪問到,而且只要Thread實(shí)例一直存在,Thread實(shí)例就強(qiáng)引用著ThreadLocalMap,因此ThreadLocalMap就不會(huì)被回收,那么這里K為null的V就一直占用著內(nèi)存。
綜上,發(fā)生內(nèi)存泄露的條件是
ThreadLocal實(shí)例沒有被外部強(qiáng)引用,比如我們假設(shè)在提交到線程池的task中實(shí)例化的ThreadLocal對(duì)象,當(dāng)task結(jié)束時(shí),ThreadLocal的強(qiáng)引用也就結(jié)束了
ThreadLocal實(shí)例被回收,但是在ThreadLocalMap中的V沒有被任何清理機(jī)制有效清理
當(dāng)前Thread實(shí)例一直存在,則會(huì)一直強(qiáng)引用著ThreadLocalMap,也就是說ThreadLocalMap也不會(huì)被GC
也就是說,如果Thread實(shí)例還在,但是ThreadLocal實(shí)例卻不在了,則ThreadLocal實(shí)例作為key所關(guān)聯(lián)的value無法被外部訪問,卻還被強(qiáng)引用著,因此出現(xiàn)了內(nèi)存泄露。
解決辦法就是創(chuàng)建ThreadLocal時(shí)將ThreadLocal變量設(shè)置成全局static類型,當(dāng)需要存儲(chǔ)線程私有的數(shù)據(jù)時(shí),通過全局的ThreadLocal變量來存,不要在普通方法體中定義局部的ThreadLocal變量來存數(shù)據(jù);不要手動(dòng)將ThreadLocal引用指向null。
更多面經(jīng)
掃描二維碼
獲取更多面經(jīng)
扶搖就業(yè)
