ThreadLocal 兩大使用場景
文章首發於公衆號,歡迎訂閱
場景一:每個線程需要一個獨享的對象(通常是工具類,典型需要使用的類有 SimpleDateFormat
和Random
)。
public class ThreadLocalDemo1 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int end = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalDemo1().date(end);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
return dateFormat.format(date);
}
}
class ThreadSafeFormatter {
// 寫法一
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
// 寫法二
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
場景二:每個線程內需要保存類似於全局變量的信息(例如在攔截器中獲取用戶信息,該信息在本線程執行的各方法中保持不變),可以讓不同方法直接使用,卻不想被多線程共享(因爲不同線程獲取到的用戶信息不一樣),避免參數傳遞的麻煩。
public class ThreadLocalDemo2 {
public static void main(String[] args) {
new Service1().process("");
}
}
class Service1 {
public void process(String name) {
User user = new User("feichaoyu");
UserContextHolder.holder.set(user);
new Service2().process();
}
}
class Service2 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service2拿到用戶名:" + user.name);
new Service3().process();
}
}
class Service3 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service3拿到用戶名:" + user.name);
UserContextHolder.holder.remove();
}
}
class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
String name;
public User(String name) {
this.name = name;
}
}
可以得出 ThreadLocal
的兩大作用:
- 讓某個需要用到的對象在線程間隔離(每個線程都有自己的獨立的對象)。
- 在任何方法中都可以輕鬆獲取到該對象。
使用 ThreadLocal 的好處
- 達到線程安全。
- 不需要加鎖,提高執行效率。
- 更高效地利用內存、節省開銷:相比於每個任務都新建一個
SimpleDateFormat
,顯然用ThreadLocal
可以節省內存和開銷。 - 免去傳參的繁瑣:無論是場景一的工具類,還是場景二的用戶名,都可以在任何地方直接通過
ThreadLocal
拿到,再也不需要每次都傳同樣的參數。Threadlocal
使得代碼耦合度更低,更優雅。
ThreadLocal 的實現原理
概述
Thread
類中有一個 threadLocals
和一個 inheritableThreadLocals
, 它們都是 ThreadLocalMap
類型的變量 。在默認情況下, 每個線程中的這兩個變量都爲 null
,只有當前線程第一次調用 ThreadLocal
的 set
或者 get
方法時纔會創建它們 。 其實每個線程的本地變量不是存放在 ThreadLocal
實例裏面,而是存放在調用線程的 threadLocals
變量裏面 。 也就是說 , ThreadLocal
類型的本地變量存放在具體的線程內存空間中 。 ThreadLocal
就是一個工具殼,它通過 set
方法把 value
值放入調用線程的 threadLocals
裏面並存放起來 , 當調用線程調用它的 get
方法時,再從當前線程的 threadLocals
變量裏面將其拿出來使用 。 如果調用線程一直不終止, 那麼這個本地變量會一直存放在調用線程的 threadLocals
變量裏面 ,所以當不需要使用本地變量時可以通過調用 ThreadLocal
變量的 remove
方法 ,從當前線程的 threadLocals
裏面刪除該本地變量 。
get 方法源碼
get
方法:
public T get() {
// 獲取當前線程t
Thread t = Thread.currentThread();
// 獲取當前線程t持有的map
ThreadLocalMap map = getMap(t);
// 如果map不爲null,返回其鍵值對中保存的value
if (map != null) {
// this指的是當前ThreadLocal,通過當前ThreadLocal獲取Entry
ThreadLocalMap.Entry e = map.getEntry(this);
// 如果Entry存在,通過Entry獲取值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 1.map爲空
// 2.map不爲空,但是還沒有存儲當前的ThreadLocalMap對象
// 則執行以下邏輯
return setInitialValue();
}
setInitialValue
方法:
private T setInitialValue() {
// 可以自己設置初值,否則默認爲null
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 如果map不爲空,則更新值
if (map != null)
map.set(this, value);
// 否則創建新的map
else
createMap(t, value);
return value;
}
createMap
方法:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
set 方法源碼
set
方法:
public void set(T value) {
// 獲取當前線程t
Thread t = Thread.currentThread();
// 獲取當前線程t持有的map
ThreadLocalMap map = getMap(t);
// 如果map不爲空,則更新值
if (map != null)
map.set(this, value);
// 否則創建新的map
else
createMap(t, value);
}
remove 方法源碼
remove
方法:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
remove(this)
方法:
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 找到要刪除的key
if (e.get() == key) {
// 去除key的軟引用
e.clear();
//
expungeStaleEntry(i);
return;
}
}
}
expungeStaleEntry
方法:
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
// 注意到這裏
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
在expungeStaleEntry
方法調用前有這麼一行代碼e.clear();
,該行代碼去除了 key 的軟引用,那麼在expungeStaleEntry
方法中我們注意到ThreadLocal<?> k = e.get();
,此時 k
爲 null
,符合下面的 if
判斷,會將e.value
置爲 null
,完成了整個鍵值對的釋放,此時就不會內存泄漏了。
ThreadLocal 原理總結
- 每個 Thread 維護着一個 ThreadLocalMap 的引用
- ThreadLocalMap 是 ThreadLocal 的內部類,用 Entry 來進行存儲
- 調用 ThreadLocal 的 set() 方法時,實際上就是往 ThreadLocalMap 設置值,key 是 ThreadLocal 對象,值是傳遞進來的對象
- 調用 ThreadLocal 的 get() 方法時,實際上就是往 ThreadLocalMap 獲取值,key 是 ThreadLocal 對象
- ThreadLocal 本身並不存儲值,它只是作爲一個 key 來讓線程從 ThreadLocalMap 獲取 value。
ThreadLocal 的內存泄漏
內存泄漏是指,某個對象不再使用,但是佔用的內存無法回收。
在 ThreadLocalMap
中有一個 Entry
內部類,它的代碼如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
在 Entry
中保存了鍵和值,鍵使用的是 WeakReference
弱引用(可以自動被 GC 回收),值使用的是強引用(無法自動被 GC 回收)。
正常情況下,當線程停止,保存在 ThreadLocal
中的 value
會被垃圾回收,因爲沒有任何強引用了。但是如果線程不停止,那麼 value
就無法被垃圾回收。
鏈路:Thread -> ThreadLocalMap -> Entry -> Value
因爲 value
和 Thread
之間還存在這個強引用鏈路,所以導致 value
無法回收,就可能會出現 OOM。
因此阿里規約中指出,在使用完 ThreadLocal
後,應該調用 remove
方法。
原理是在 remove
方法中,會調用 expungeStaleEntry
方法,這個方法的意思是清除老的Entry:
if (k == null) {
e.value = null;
}
我創建了一個免費的知識星球,用於分享知識日記,歡迎加入!