什么是ThreadLocal
ThreadLocal是一個用于創(chuàng)建線程局部變量的類衰抑,它有兩個特點愕撰,其一對于線程A創(chuàng)建的數(shù)據(jù)只有線程A能獲取和修改;其二只要能獲取到ThreadLocal的示例,線程就能獲取到其中的值不翩。
使用場景
網(wǎng)上有介紹ThreadLocal與synchronized對比的文章,但是我覺得它們之間并沒有可以性麻裳。ThreadLocal注重的點在于通過線程獨有空間來存儲和獲取數(shù)據(jù)口蝠,而synchronized注重的是多線程同時對同一變量的獲取與修改。
簡單的比喻是ThreadLocal好比每個人(線程)有自己的口袋存津坑,每個人只通過自己口袋來存放和取東西妙蔗。而synchronized就好比所有人(多有線程)都在同一個口袋存放和取東西,但是一次只能一個人操作疆瑰。
public class App6 {
public static Map<Thread,String> globalMap = new HashMap<>();
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
Thread currentThread = Thread.currentThread();
//存數(shù)據(jù)
globalMap.put(currentThread,"abc");
//取數(shù)據(jù)
System.out.printf("[%s]=[%s]\n",currentThread.getName(),globalMap.get(currentThread));
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
Thread currentThread = Thread.currentThread();
//存數(shù)據(jù)
globalMap.put(currentThread,"123");
//取數(shù)據(jù)
System.out.printf("[%s]=[%s]\n",currentThread.getName(),globalMap.get(currentThread));
}
}).start();
TimeUnit.SECONDS.sleep(1);
System.out.println(globalMap);
}
}
上面的示例中眉反,每個線程將自己的數(shù)據(jù)放在Map中,然后獲取自己數(shù)據(jù)穆役。但是上面的示例是線程不安全的寸五,千萬不要在自己的代碼中使用。如果使用上面的方案耿币,我們需要使用線程安全的Map或者加鎖來解決梳杏。即使解決了線程安全的問題還存在兩個問題,其一就是Map中的值并不是當(dāng)前線程獨有的淹接,其他線程也是可以獲取和修改它十性。其二為了保證線程安全Map需要加鎖,在性能上時有損失的塑悼。
上面所提到的問題使用ThreadLocal都可以解決【⑹剩現(xiàn)在修改代碼如下:
public class App7 {
public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
Thread currentThread = Thread.currentThread();
//存數(shù)據(jù)
threadLocal.set("abc");
//取數(shù)據(jù)
System.out.printf("[%s]=[%s]\n",currentThread.getName(),threadLocal.get());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
Thread currentThread = Thread.currentThread();
//存數(shù)據(jù)
threadLocal.set("123");
//取數(shù)據(jù)
System.out.printf("[%s]=[%s]\n",currentThread.getName(),threadLocal.get());
}
}).start();
TimeUnit.SECONDS.sleep(1);
System.out.println(threadLocal.get());
}
}
通過使用ThreadLocal,它既能保證多線程訪問的安全性,同時也能實現(xiàn)無鎖。
實現(xiàn)原理
要想實現(xiàn)ThreadLocal這樣的效果我們基本山能想到的就兩種方式方式來實現(xiàn)婿奔。
ThreadLocal維護(hù)Thread與數(shù)據(jù)的映射關(guān)系
該實現(xiàn)方式是在ThreadLocal內(nèi)部維護(hù)一個線程安全的Map荒给,然后以當(dāng)前線程作為Map的Key胖烛,而線程的數(shù)據(jù)作為Value宽档。
上面的這種方式能實現(xiàn)ThreadLocal的功能讲岁,但是問題在于通過這種方式實現(xiàn)就必須要保證ThreadLocal中負(fù)責(zé)維護(hù)線程和數(shù)據(jù)的Map線程安全主之,這或多或少都需要增加鎖的引入颂鸿,并不能實現(xiàn)無鎖促绵。
Thread維護(hù)ThreadLocal與數(shù)據(jù)的映射關(guān)系
通過上面的分析我們知道如果在ThreadLocal中維護(hù)Thread與數(shù)據(jù)的映射我們需要必須要保證內(nèi)部映射關(guān)系的線程安全,如果我們在Thread內(nèi)存維護(hù)一個ThreadLocal與數(shù)據(jù)之前的映射關(guān)系嘴纺,這種映射關(guān)系并沒有涉及到線程安全問題败晴,這樣也就省去了線程同步的操作,相比上面的實現(xiàn)方式栽渴,該方式性能上更好尖坤。而JDK內(nèi)部就是使用該方式來實現(xiàn)的。
基本使用
實例化
示例化方式一般就兩種闲擦,第一種是直接使用無參構(gòu)造函數(shù)創(chuàng)建慢味,第二種則是在1.8的版本提供的靜態(tài)方法創(chuàng)建。
public class App8 {
/**
* 實例化方式1 構(gòu)造函數(shù)
*/
public static ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
/**
* 實例化方式2 靜態(tài)方法,
*/
public static ThreadLocal<String> threadLocal2 = ThreadLocal.withInitial(() -> {
System.out.println("如果值為空時則會調(diào)用該方法獲取初始值");
return "abc";
});
public static void main(String[] args) {
System.out.println(threadLocal1.get());
System.out.println(threadLocal2.get());
threadLocal2.remove();
System.out.println(threadLocal2.get());
}
}
常用方法
常用的方法就三個墅冷,set()用來往ThreadLocal中設(shè)置值纯路,get()用來往ThreadLocal中獲取值,而remove()用來刪除ThreadLocal中的值寞忿。
public class App9 {
public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
//設(shè)置值
threadLocal.set("abc");
//獲取值
String value = threadLocal.get();
System.out.println(value);
//刪除值
threadLocal.remove();
System.out.println(threadLocal.get());
}
}
ThreadLocal中提供的方法比較簡單驰唬,但是在使用時需要特別注意remove方法。當(dāng)我們使用完ThreadLocal后我們應(yīng)該調(diào)用remove方法將ThreadLocal中的數(shù)據(jù)清除腔彰,如果不這么做容易產(chǎn)生業(yè)務(wù)數(shù)據(jù)異常和內(nèi)存泄漏(后面將說明為什么會導(dǎo)致內(nèi)存泄漏)叫编。
public class App10 {
public static ThreadLocal<List<Integer>> threadLocal = ThreadLocal.withInitial(() -> new ArrayList<>());
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
for (int i = 0; i < 10; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
int value = new Random().nextInt(1000);
threadLocal.get().add(value);
System.out.println(threadLocal.get());
}
});
}
executor.shutdown();
}
}
最后的打印結(jié)果如下:
[562]
[725]
[562, 434]
[725, 590]
[562, 434, 448]
[725, 590, 377]
[562, 434, 448, 712]
[725, 590, 377, 57]
[562, 434, 448, 712, 715]
[725, 590, 377, 57, 580]
因為我們是在線程池中使用ThreadLocal,而線程池中的線程并不是執(zhí)行完之后就銷毀了霹抛。而代碼中并沒有調(diào)用remove方法清除ThreadLocal中的值搓逾,這就導(dǎo)致了List中保留了上一次任務(wù)的執(zhí)行結(jié)果。
源碼分析
我們在實現(xiàn)原理中大致講過ThreadLocal是如何實現(xiàn)的杯拐,我們先看如何往ThreadLocal中存儲值的霞篡。
public void set(T value) {
//獲取當(dāng)前的線程
Thread t = Thread.currentThread();
//獲取Map
ThreadLocalMap map = getMap(t);
if (map != null)
//往Map中放值,Map的key就是當(dāng)前的ThreadLocal實例
map.set(this, value);
else
//如果Map為空則創(chuàng)建Map,并將值放入Map中
createMap(t, value);
}
上面代碼中的getMap(t)方法的實現(xiàn)如下:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
代碼很簡單藕施,返回的就是ThreadLocal中的字段threadLocals寇损,它的類型就是ThreadLocalMap凸郑。我們調(diào)用set方法就是往Map中存放存放值裳食,而這個Map的key就是ThreadLocal,它的值就是我們要存的值芙沥。
public T get() {
//獲取當(dāng)前線程
Thread t = Thread.currentThread();
//獲取Map
ThreadLocalMap map = getMap(t);
if (map != null) {
//獲取Entry中的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果Map為空則獲取withInitial中supplier返回的值
return setInitialValue();
}
為何會內(nèi)存泄漏
通過上面我們知道了ThreadLocal的實現(xiàn)原理诲祸,但是為何說會內(nèi)存泄漏呢浊吏?我們先看ThreadLocalMap的結(jié)構(gòu)。
上圖就是ThreadLocal的結(jié)構(gòu)了救氯,它內(nèi)部提供了增刪改等主要方法找田,而ThreadLocal與值被封裝成Entry對象存放在table數(shù)組中。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
從Entry中的結(jié)構(gòu)可以知道着憨,每一個Entry對象都是一個對Key弱的引用(關(guān)于什么是弱引用可以參考該文)墩衙,當(dāng)沒有強引用指向ThreadLocal變量時,ThreadLocal可以被回收甲抖。真是通過這種方式保證了ThreadLocal在沒有引用時而Thread還沒有被銷毀時可以被回收漆改。但是上面的問題帶來了另一個問題,當(dāng)Key被回收之后Entry對象并沒有被回收而導(dǎo)致內(nèi)存泄漏准谚。
如何解決
對于ThreadLocal中存在的內(nèi)存泄漏問題挫剑,最簡單的解決方案就是我們在每次在使用完ThreadLocal后手動的調(diào)用remove清除數(shù)據(jù)。但是如果你沒有這么做柱衔,ThreadLocal對于存在的內(nèi)存泄漏問題也做了部分優(yōu)化樊破。在set和get方法中,都會間接或直接的調(diào)用cleanSomeSlots唆铐、expungeStaleEntry哲戚、replaceStaleEntry等方法將key為空的Entry清除掉。雖然對于內(nèi)存泄漏ThreadLocal內(nèi)部已經(jīng)做了優(yōu)化或链,但是我們在使用最好還是在ThreadLocal不再使用時手動調(diào)用remove方法清除掉其中的數(shù)據(jù)惫恼,從而避免內(nèi)存泄漏。