目錄
1 ThreadLocal概述
我們知道單例模式有一個明顯的缺點,就是存在併發問題。多線程修改、訪問同一個對象的時候會出現與預期不相符的結果。解決併發問題可以通過加鎖,但是存在性能問題。
另一個方式就是可以用ThreadLocal,把對象做成一個線程級別的變量,每個線程都維護一個數據的副本,實現線程級別的數據隔離,也是典型的拿空間換時間。
2 ThreadLocal使用
@Test
public void testThreadLocal() throws InterruptedException {
new Thread(() -> {
setData(1);
getData();
}, "線程一").start();
new Thread(() -> {
setData(2);
getData();
}, "線程二").start();
TimeUnit.MINUTES.sleep(1);
}
private void setData(Integer d) {
data.set(d);
System.out.println(Thread.currentThread().getName() + " set data:" + data.get());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void getData() {
System.out.println(Thread.currentThread().getName() + " get data:" + data.get());
}
輸出:
線程一 set data:1
線程二 set data:2
線程一 get data:1
線程二 get data:2
可以看到每個線程設置、訪問數據是不收影響的
3 ThreadLocal原理
3.1 set方法
public void set(T value) {
Thread t = Thread.currentThread();
// 獲取當前線程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
// 如果map不爲空則存入數據,key-ThreadLocal對象,v-存放的數據
map.set(this, value);
else
// 如果map爲空則創建一個map
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
閱讀源碼可知每個線程維護了一個map,這個map用來存放ThreadLocal和數據的映射關係。存放數據的時候首先獲取當前線程的map,然後在map中存放數據
3.2 get方法
public T get() {
// 獲取當前線程
Thread t = Thread.currentThread();
// 獲取當前線程的map
ThreadLocalMap map = getMap(t);
if (map != null) {
// 獲取該ThradLocal對象綁定的entry,進而獲得數據
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
get方法很簡單,就是先根據當前線程獲取map,然後根據map讀取的對象
3.3 remove方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
remove方法就是把當前線程綁定的數據移除,比較簡單,這裏不做太多分析
4 使用ThreadLocal注意事項
一般我們項目中都會使用到線程池,線程池中的線程是可以複用的,所以每次使用完ThreadLocal的時候要清理一下數據,否則用線程池執行下一個任務的時候可能會獲取到舊數據
測試代碼:
private ThreadLocal<Integer> data = new ThreadLocal<>();
private Executor executor = Executors.newFixedThreadPool(1);
@Test
public void testThreadLocal() throws InterruptedException {
executor.execute(() -> {
setData(1);
getData();
});
executor.execute(() -> getData());
TimeUnit.MINUTES.sleep(1);
}
private void setData(Integer d) {
data.set(d);
System.out.println(Thread.currentThread().getName() + " set data:" + data.get());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void getData() {
System.out.println(Thread.currentThread().getName() + " get data:" + data.get());
}
輸出:
pool-1-thread-1 set data:1
pool-1-thread-1 get data:1
pool-1-thread-1 get data:1
可以看到線程池執行第二個任務的時候仍然獲取到第一個任務中存放的數據
get數據的正確姿勢(當然移除的時候確定後續過程不再需要訪問data了)
private void getData() {
Integer d = data.get();
data.remove();
System.out.println(Thread.currentThread().getName() + " get data:" + d);
}
再測試:
pool-1-thread-1 set data:1
pool-1-thread-1 get data:1
pool-1-thread-1 get data:null