目錄
- ThreadLocal是什麼
- 如何使用ThreadLocal
- ThreadLocal源碼分析
- 消息機制中Looper中的ThreadLocal使用
- 資料
- 收穫
上一篇我們分析了Anrdoid消息機制的實現,其中關於ThreadLocal以及Native層的還沒有搞清楚,這篇我們來一起學習分析下ThreadLocal的作用。
一、ThreadLocal是什麼
ThreadLocal 線程局部變量 是一個泛型類,可以接受任何類型的對象,一般ThreadLocal的類型的變量時static類型的。
我們知道不同線程有自己的棧,但是內存資源在同一個進程是共享的,即不同線程可以訪問同一個變量,這樣就會有多線程同步問題。即一個線程修改了變量,另外一個線程再讀,如果不加鎖或者volatile,可能導致不同線程的獲取的結果不一致。
想象下面一種場景:滿足下面兩個條件
- 一個對象中的一個變量,會在不同的方法中會使用,這個對象會在不同線程中調用。
- 這個變量不需要多線程同步,而是需要每個線程都一份獨立的值,即是線程隔離的。
這是我們該如何設計吶?
可能我們首先想到的是通過Map的方式,Key來存儲Thread(eg:ThreadId),Value來存儲每個Thread中該變量的值。在對map的讀寫操作上加上同步鎖,即可實現上面場景的需求。
但是這種方案由於加了鎖,會帶來一定的性能損耗,是否還有更好的方式來實現線程隔離吶?
今天分析的ThreadLocal就是爲此而設計的,它適用於每個線程需要自己獨立的實例且該實例需要在多個方法中被使用,也即變量在線程間隔離而在方法或類間共享的場景。
使用ThreadLocal修飾的變量,在每個線程內都有自己副本,且該副本只能在自己的線程使用,實現了線程隔離。
二、如何使用ThreadLocal
這一小節,我們通過一個簡單測試代碼來說明ThreadLocal使用和驗證它的線程隔離的特性。
在一個類中定義ThreadLocal類型的變量,分別在不同的線程賦不同的值,然後輸出看下不同線程之間是否有影響。
public class ExampleUnitTest {
//定義兩個不同類型的ThreadLocal
private static ThreadLocal<String> sStrThreadlocal = new ThreadLocal<>();
private static ThreadLocal<Integer> sIntegerThreadLocal = new ThreadLocal<>();
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
@Test
public void testThreadLocal(){
//在主線程給Threadlocal賦值,請取出輸出
sStrThreadlocal.set("aaa");
String value = sStrThreadlocal.get();
sIntegerThreadLocal.set(1);
int intValue = sIntegerThreadLocal.get();
System.out.println("111 curThreadId="+Thread.currentThread()+" strthreadLocalValue="+value
+" intThreadLocalValue="+intValue);
//創建兩個線程,分別給ThreadLocal賦不同的值
new Thread(new Runnable() {
@Override
public void run() {
sStrThreadlocal.set("bbb");
String value = sStrThreadlocal.get();
sIntegerThreadLocal.set(2);
int intValue = sIntegerThreadLocal.get();
System.out.println("222 curThreadId="+Thread.currentThread()+" strthreadLocalValue="+value
+" intThreadLocalValue="+intValue);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
String value = sStrThreadlocal.get();
Integer intValue = sIntegerThreadLocal.get();
System.out.println("333 curThreadId="+Thread.currentThread()+" strthreadLocalValue="+value
+" intThreadLocalValue="+intValue);
}
}).start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//最後在輸出下主線程的ThreadLocal值
value = sStrThreadlocal.get();
intValue = sIntegerThreadLocal.get();
System.out.println("444 curThreadId="+Thread.currentThread()+" strthreadLocalValue="+value
+" intThreadLocalValue="+intValue);
}
}
運行結果如下:
111 curThreadId=Thread[main,5,main] strthreadLocalValue=aaa intThreadLocalValue=1
222 curThreadId=Thread[Thread-0,5,main] strthreadLocalValue=bbb intThreadLocalValue=2
333 curThreadId=Thread[Thread-1,5,main] strthreadLocalValue=null intThreadLocalValue=null
444 curThreadId=Thread[main,5,main] strthreadLocalValue=aaa intThreadLocalValue=1
不同線程給ThreadLocal修飾的變量賦不同的值,在每個線程得到的值不同的,的確實現了線程的隔離。
那麼它是如何做到的吶?是否是通過HashMap來存儲不同線程的value值吶?我們通過分析ThreadLocal源碼來找下答案。
三、ThreadLocal源碼分析
ThreadLocal 是一個泛型類,可以接受任何類型的對象
正如上面的示例代碼所示,一個線程內可以存在多個 ThreadLocal 對象,而ThreadLocal 內部維護了一個 Map ,滿足這種需求。
但是這個 Map 不是直接使用的 HashMap ,而是 ThreadLocal 實現的一個叫做 ThreadLocalMap 的靜態內部類
通過上面示例我們可以看到 通過set方式給ThreadLocal設置數據,get方法獲取數據,我們以此爲入口來進行分析
ThreadLocal#set
public void set(T value) {
//獲取調用方所在的線程
Thread t = Thread.currentThread();
//獲取該線程的ThreadLocal的副本,這個getMap方法是關鍵
ThreadLocalMap map = getMap(t);
//如果該線程存在該ThreadLocal的副本,則存入到map中,key,否則創建
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
getMap
獲取該線程的ThreadLocal的副本
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
來看下Thread類,發現threadLocals變量的類型是ThreadLocal.ThreadLocalMap,即ThreadLocal的一個靜態內部類
每個Thread對象內部都維護了一個ThreadLocalMap, 其可以存放若干個ThreadLocal
public class Thread implements Runnable {
......
//當前線程的ThreadLocalMap,主要存儲該線程自身的ThreadLocal,本文主要討論這個變量
ThreadLocal.ThreadLocalMap threadLocals = null;
//自父線程繼承而來的ThreadLocalMap,主要用於父子線程間ThreadLocal變量的傳遞
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
.....
}
再來看下ThreadLocal.ThreadLocalMap
static class ThreadLocalMap {
.......
private ThreadLocal.ThreadLocalMap.Entry[] table;
ThreadLocal.ThreadLocalMap.Entry
Entry的key是ThreadLocal的弱引用,value是對應的線程中線程局部變量set的值。
我們知道弱引用在GC的時候會銷燬該引用所包裹(引用)的對象,這個threadLocal作爲key可能被銷燬(如果沒有強引用存在),如果key爲空,則該entry會從table中刪除
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> var1, Object var2) {
super(var1);
this.value = var2;
}
}
圖片來自深入解析ThreadLocal 詳解、實現原理、使用場景方法以及內存泄漏防範 多線程中篇(十七)
從本質來講,就是每個線程都維護了一個map,而這個map的key就是threadLocal,而值就是我們set的那個值
分析完了set鏈路,我們再來看下get鏈路
當我們在調用get()方法的時候,先獲取當前線程,然後獲取到當前線程的ThreadLocalMap對象,如果非空,那麼取出ThreadLocal的value,否則進行初始化,初始化就是將initialValue的值set到ThreadLocal中。
public T get() {
//先獲取當前線程
Thread t = Thread.currentThread();
//獲取到當前線程的ThreadLocalMap對象
ThreadLocalMap map = getMap(t);
if (map != null) {
//如果非空,那麼取出ThreadLocal的value
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
//否則進行初始化,初始化就是將initialValue的值set到ThreadLocal中
return setInitialValue();
}
四、消息機制中Looper中的ThreadLocal使用
在Android的Framework中很多地方都使用了ThreadLocal,比如Looper、Choreographer、ActivityThread、ContentProvide、ViewRootImpl、SQLiteDatabase等等,在調用鏈追蹤方面也是可以使用。
我們來分析下每個線程的Looper保證獨立,並且一個線程有且只有一個Looper的
public final class Looper {
......
// sThreadLocal.get() 將會返回 null,直到調用了 prepare().
// sThreadLocal是一個ThreadLocal的一個實例,其類型爲Looper
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
final MessageQueue mQueue;
......
}
通過ThreadLocal的線程隔離 保證每個線程的Looper是不同的,
通過sThreadLocal.get() != null的異常斷言,保證了一個線程只能有一個Looper
//初始化,將當前線程初始化爲循環器
private static void prepare(boolean quitAllowed) {
//通過ThreadLocal的線程隔離 保證每個線程的Looper是不同的,
//通過sThreadLocal.get() != null的異常斷言,保證了一個線程只能有一個Looper
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
/**
* 返回調用該方法所在線程對應的Looper
* 如果調用者的線程還沒有和Looper關聯(通過preprea),則返回空
*/
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
五、資料
六、收穫
通過對本篇的學習實踐
- 瞭解了ThreadLocal的意義以及原理
- ThreadLocal的使用場景,它適用於每個線程需要自己獨立的實例且該實例需要在多個方法中被使用,也即變量在線程間隔離而在方法或類間共享的場景。
- Android消息機制中通過ThreadLocal保證Looper的線程隔離,get是斷言判斷保證一個Thread只能有一個Looper。
感謝你的閱讀
下一篇我們分析消息機制的Native層,分析瞭解是如何阻塞和喚醒的,歡迎關注公衆號“音視頻開發之旅”,一起學習成長。
歡迎交流