前言
ThreadLocal工作中會偶有用到,是解決thread間的數據隔離問題的(並不是爲解決併發和共享問題的),也是面試常見問題,比如:ThreadLocal知道嗎?說說你自己的理解?或者這樣問:在多線程環境下,如何防止自己的變量被其它線程篡改?無論基於哪種原因都是很有必要學習的。
ThreadLocal是什麼
貼一段源碼中的介紹:這個類提供線程的局部變量,可以通過get()和set()方法來獲取和設置自己的局部變量;ThreadLocal實例通常是pricate static fields的,希望將信息關聯到一個線程中,例如:user ID、Transaction ID
概括的說:存儲各個線程互不相同的信息,實現線程間的數據隔離。
/**
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code ThreadLocal} instances are typically private
* static fields in classes that wish to associate state with a thread (e.g.,
* a user ID or Transaction ID).
*/
ThreadLocal能做什麼
-
實例1:參數傳遞(用戶信息傳遞)
當前用戶信息需要被線程內的所有方法共享
-
方案1:傳遞參數
將user作爲參數在每個方法中進行傳遞,缺點:會產生代碼冗餘問題,並且可維護性差
-
方案2:使用Map
缺點:如果在單線程環境下可以保證安全,但是在多線程環境下是不可以的。如果使用加鎖或是ConcurrentHashMap都會產生性能問題。
-
方案3:使用ThreadLocal,實現不同方法間的資源共享
-
public class ThreadLocalNormalUsage02 {
public static void main(String[] args) {
new Service1().process();
}
}
class Service1 {
public void process() {
User user = new User("張三");
//將User對象存儲到 holder 中
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);
}
}
class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
String name;
public User(String name) {
this.name = name;
}
}
結果:
總結:實際項目中也會用到這個實例,在請求的攔截器中添加用戶信息到ThreadLocal< User >的ThreadLocalMap中,這樣同一線程中的各個方法或組建就可以獲取到對應的用戶信息。
- 實例2:典型工具類(SimpleDateFormat和Random)
class ThreadSafeFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
//創建一份 SimpleDateFormat 對象
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
}
};
}
ThreadLocal爲每個線程創建獨立的SimpleDateFormat對象(實際應用中,針對不通的format形式,會創建不同的方法,方法中會是一個新SimpleDateFormat對象),當然也可以使用LocalDateTime。
注:可參考這篇博客——地址
ThreadLocal原理
- 首先看set()方法:
public void set(T value) {
//獲取當前線程
Thread t = Thread.currentThread();
//獲取ThreadLocalMap,這個map是什麼那?下面有介紹
ThreadLocalMap map = getMap(t);
if (map != null)
//如果map存在,則以當前線程t爲key,數據爲value放到map
map.set(this, value);
else
//否則創建新的map再存放數據
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
再來看一下ThreadLocalMap
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
//map的其他定義
}
ThreadLocalMap實際上是ThreadLocal的一個靜態內部類,數據就存放在Entry中;
那這個map如何從Thread中獲取的那?看下面getMap()和Thread類中的代碼
//ThreadLocal中的getMap方法
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* Thread的threadLocals
*/
ThreadLocal.ThreadLocalMap threadLocals = null;
實際上Thread類中維護了一個ThreadLocalMap變量。
如果map不存在,則創建一個,源碼如下:
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
- 大致瞭解set的過程,再來看get過程就比較容易理解了
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
//獲取線程對象t
Thread t = Thread.currentThread();
//在對象t中獲取ThreadLocalMap
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;
}
}
return setInitialValue();
}
如果沒有獲取map(可能沒有set值),則初始化value值
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
實際上value是null。
- 刪除數據:
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
- 總結:ThreadLocal實現數據隔離主要是依賴ThreadLocalMap,而每一個線程中都有一個ThreadLocal.ThreadLocalMap變量,這樣在set和get數據時,都是獲取對應線程中的ThreadLocalMap,數據則存在map裏面的Entry中,key爲Threalocal對象。
關於ThreadLocal內存泄漏
-
內存泄漏的原因:
- 個人認爲ThreadLocal內存泄漏只是有可能,是個小概率事件
- 內存泄露:某個對象不會再被使用,但是該對象的內存卻無法被收回
static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { //調用父類,父類是一個弱引用 super(k); //強引用 value = v; } }
-
強引用:當內存不足時觸發GC,寧願拋出OOM也不會回收強引用的內存
-
弱引用:觸發GC後便會回收弱引用的內存
-
分析:
- 正常情況:當Thread運行結束後,ThreadLocal中的value會被回收,因爲沒有任何強引用了
- 非正常情況:當Thread一直在運行始終不結束,強引用就不會被回收,存在以下調用鏈——Thread–>ThreadLocalMap–>Entry(key爲null)–>value;因爲調用鏈中的 value 和 Thread,存在強引用,所以value無法被回收,就有可能出現OOM。
JDK的設計已經考慮到了這個問題,所以在set()、remove()、resize()方法中會掃描到key爲null的Entry,並且把對應的value設置爲null,這樣value對象就可以被回收。
-
如何避免內存泄漏:
調用remove()方法,就會刪除對應的Entry對象,可以避免內存泄漏,所以使用完ThreadLocal後,要調用remove()方法。
NPE問題
public class ThreadLocalNPE {
ThreadLocal<Long> longThreadLocal = new ThreadLocal<>();
public void set() {
longThreadLocal.set(Thread.currentThread().getId());
}
public Long get() {
return longThreadLocal.get();
}
public static void main(String[] args) {
ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
//如果get方法返回值爲基本類型,則會報空指針異常,如果是包裝類型就不會出錯
System.out.println("out:" + threadLocalNPE.get());
new Thread(new Runnable() {
@Override
public void run() {
threadLocalNPE.set();
System.out.println("inner:" + threadLocalNPE.get());
}
}).start();
}
}
上面例子出現NPE問題,主要是get方法返回值類型,我們知道在未對ThreadLocalMap進行set值時,如果直接獲取,代碼會給我們初始化一個null保存到map中;如果上面get方法返回值是基本數據類型,則會出現裝箱和拆箱,導致NPE。
共享數據問題
如果在每個線程中ThreadLocal.set()進去的東西本來就是多個線程共享的同一對象,比如static對象,那麼多個線程調用ThreadLocal.get()獲取的內容還是同一個對象,還是會發生線程安全問題
總結
可以不使用ThreadLocal就不要強行使用:如果在任務數很少的時候,在局部方法中創建對象就可以解決問題,這樣就不需要使用ThreadLocal。
優先使用框架的支持,而不是自己創造:例如在Spring框架中,如果可以使用RequestContextHolder,那麼就不需要自己維護ThreadLocal,因爲自己可能會忘記調用remove()方法等,造成內存泄漏。
參考博文:
https://juejin.im/post/5e0d8765f265da5d332cde44#heading-5
https://juejin.im/post/5ac2eb52518825555e5e06ee#heading-4
https://juejin.im/post/5e0d8765f265da5d332cde44#heading-13
https://www.jianshu.com/p/6bf1adb775e0
https://www.jianshu.com/p/98b68c97df9b
https://blog.csdn.net/lufeng20/article/details/24314381?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
https://www.jianshu.com/p/377bb840802f