1、ThreadLocal是什麼?
ThreadLocal
是一個執行緒內部資料儲存類,通過他可以在指定的執行緒中儲存資料。儲存後,只能在指定的執行緒中獲取到儲存的資料,對其他執行緒來說無法獲取到資料。
2、ThreadLocal的使用場景
日常使用場景不多,當某些資料是以執行緒為作用域並且不同執行緒具有不同的資料副本的時候,可以考慮使用ThreadLocal
。
Android
原始碼的Lopper
、ActivityThread
以及AMS
中都用到了ThreadLocal
。
3、ThreadLocal的使用示例
public class ThreadLocalActivity extends AppCompatActivity {
private ThreadLocal<String> name = new ThreadLocal<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_thread_local);
name.set("小明");
Log.d("ThreadLocalActivity", "Thread:" + Thread.currentThread().getName() + " name:" + name.get());
new Thread("thread1") {
@Override
public void run() {
name.set("小紅");
Log.d("ThreadLocalActivity", "Thread:" + Thread.currentThread().getName() + " name:" + name.get());
}
}.start();
new Thread("thread2") {
@Override
public void run() {
Log.d("ThreadLocalActivity", "Thread:" + Thread.currentThread().getName() + " name:" + name.get());
}
}.start();
}
}
複製程式碼
執行結果:
D/ThreadLocalActivity: Thread:main name:小明
D/ThreadLocalActivity: Thread:thread1 name:小紅
D/ThreadLocalActivity: Thread:thread2 name:null
複製程式碼
可以看到雖然訪問的是同一個ThreadLocal
物件,但是獲取到的值卻是不一樣的。
4、ThreadLocal的原始碼閱讀
那麼為什麼會造成這樣的結果呢?這就需要去看看ThreadLocal
的原始碼實現,這裡的原始碼版本為API28
。主要看它的get
和set
方法。
set
方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
複製程式碼
set
方法中首先獲取了當前執行緒物件,然後通過getMap
方法傳入當前執行緒t
獲取到一個ThreadLocalMap
,接下來判斷這個map
是否為空,不為空就直接將當前ThreadLocal
作為key
,set
方法中傳入要儲存的值最為value
,存放到map
中;如果map
為空就呼叫createMap
方法建立一個map
並同樣將當前ThreadLocal
和要儲存的值作為key
和value
加入到map
中。
接下先看getMap
方法:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
複製程式碼
getMap
方法比較簡單,就是返回從傳入的當前執行緒物件的成員變數threadLocals
。
接著是createMap
方法:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
複製程式碼
createMap
方法也很簡單就是new
了一個ThreadLocalMap
並賦給當前執行緒物件t
中的threadLocals
。
原來這個Map
是存放在Thread
類中的。於是進入Thread
類中檢視。
Thread.java
第188-190行:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
複製程式碼
根據這裡的註釋可以得知,每個執行緒Thread
中都有一個ThreadLocalMap
型別的threadLocals
成員變數來儲存資料,通過ThreadLocal
類來進行維護。這樣看來我們每次在不同執行緒呼叫ThreadLocal
的set
方法set
的資料是存在不同執行緒的ThreadLocalMap
中的,就像註釋說的ThreadLocal
只是起了個維護ThreadLocalMap
的功能。想到是get
方法同樣也是到不同執行緒的ThreadLocalMap
去取資料。
get
方法:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
複製程式碼
果然,get
方法中同樣是先獲取當前執行緒物件,然後在拿著這個物件t
去獲取到t
中的ThreadLocalMap
,只要map
不等於null
就呼叫map.getEntry(this)
方法來獲取資料,因為ThreadLocalMap
裡使用一個內部類Entry
來儲存資料的,所以呼叫getEntry(this)
方法,傳入的key
是當前的ThreadLocal
。這樣獲取到Entry
型別資料e
,只要e
不為null
,返回e.value
即先前儲存的資料。如果獲取到的map
為null
又或者根據key
獲取Entry
為null
,就呼叫setInitialValue
方法初始化一個value
返回。
setInitialValue
和initialValue
方法:
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;
}
複製程式碼
setInitialValue
方法中首先呼叫initialValue
方法初始化了一個空value
,之後的操作和set
方法相同,將這個空的value
加入到當前執行緒的ThreadLocalMap
中去,ThreadLocalMap
為空就建立個Map
,最後返回這個空值。
至此,ThreadLocal
的get
、set
方法就都看過了,也理解了ThreadLocal
可以在多個執行緒中操作而互不干擾的原因。但是ThreadLocal
還有一個要注意的地方就是ThreadLocal
使用不當會造成記憶體洩漏。
5、ThreadLocal記憶體洩漏的原因
記憶體洩漏的根本原因是當一個物件已經不需要再使用本該被回收時,另外一個正在使用的物件持有它的引用從而導致它不能被回收,導致本該被回收的物件不能被回收而停留在堆記憶體中。那麼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
類,這裡的key
是使用了個弱引用,所以因為使用弱引用這裡的key
,ThreadLocal
會在JVM
下次GC
回收時候被回收,而造成了個key
為null
的情況,而外部ThreadLocalMap
是沒辦法通過null
key
來找到對應value
的。如果當前執行緒一直在執行,那麼執行緒中的ThreadLocalMap
也就一直存在,而map
中卻存在key
已經被回收為null
對應的Entry
和value
卻一直存在不會被回收,造成記憶體的洩漏。
不過,這一點設計者也考慮到了,在get()
、set()
、remove()
方法呼叫的時候會清除掉執行緒ThreadLocalMap
中所有Entry
中Key
為null
的Value
,並將整個Entry
設定為null
,這樣在下次回收時就能將Entry
和value
回收。
這樣看上去好像是因為key
使用了弱引用才導致的記憶體洩漏,為了解決還特意新增了清除null key
的功能,那麼是不是不用弱引用就可以了呢?
很顯然不是這樣的。設計者使用弱引用是由原因的。
- 如果使用強引用,那麼如果在執行的執行緒中
ThreadLocal
物件已經被回收了但是ThreadLocalMap
還持有ThreadLocal
的強引用,若是沒有手動刪除,ThreadLocal
不會被回收,同樣導致記憶體洩漏。 - 如果使用弱引用
ThreadLocal
的物件被回收了,因為ThreadLocalMap
持有的是ThreadLocal
的弱引用,即使沒有手動刪除,ThreadLocal
也會被回收。nullkey
的value
在下一次ThreadLocalMap
呼叫set
、get
、remove
的時候會被清除。
所以,由於ThreadLocalMap
和執行緒Thread
的生命週期一樣長,如果沒有手動刪除Map
的中的key
,無論使用強引用還是弱引用實際上都會出現記憶體洩漏,但是使用弱引用可以多一層保護,null key
在下一次ThreadLocalMap
呼叫set
、get
、remove
的時候就會被清除。
因此,ThreadLocal
的記憶體內洩漏的真正原因並不能說是因為ThreadLocalMap的key
使用了弱引用,而是因為ThreadLocalMap
和執行緒Thread
的生命週期一樣長,沒有手動刪除Map
的中的key
才會導致記憶體洩漏。所以解決ThreadLocal
的記憶體洩漏問題就要每次使用完ThreadLocal
,都要記得呼叫它的remove()
方法來清除。