前言
線程并發(fā)系列文章:
Java 線程基礎
Java 線程狀態(tài)
Java “優(yōu)雅”地中斷線程-實踐篇
Java “優(yōu)雅”地中斷線程-原理篇
真正理解Java Volatile的妙用
Java ThreadLocal你之前了解的可能有誤
Java Unsafe/CAS/LockSupport 應用與原理
Java 并發(fā)"鎖"的本質(一步步實現(xiàn)鎖)
Java Synchronized實現(xiàn)互斥之應用與源碼初探
Java 對象頭分析與使用(Synchronized相關)
Java Synchronized 偏向鎖/輕量級鎖/重量級鎖的演變過程
Java Synchronized 重量級鎖原理深入剖析上(互斥篇)
Java Synchronized 重量級鎖原理深入剖析下(同步篇)
Java并發(fā)之 AQS 深入解析(上)
Java并發(fā)之 AQS 深入解析(下)
Java Thread.sleep/Thread.join/Thread.yield/Object.wait/Condition.await 詳解
Java 并發(fā)之 ReentrantLock 深入分析(與Synchronized區(qū)別)
Java 并發(fā)之 ReentrantReadWriteLock 深入分析
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(原理篇)
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(應用篇)
最詳細的圖文解析Java各種鎖(終極篇)
線程池必懂系列
在Android事件驅動Handler-Message-Looper解析中提到過ThreadLocal,由于篇幅限制沒有展開來說拔疚,這次來一探究竟耿币。
通過這篇文章你將知道:
1率拒、為什么需要ThreadLocal
2吏够、ThreadLocal應用場景
3氢哮、ThreadLocal和線程安全關系
4句占、ThreadLocal原理解析
5淫僻、ThreadLocal在Android源碼里的運用
6岛杀、ThreadLocal會內存泄漏嗎
線程本地變量
先從一個簡單的例子開始:
class PersonHeight {
float height;
public PersonHeight(float height) {
this.height = height;
}
public float getHeight() {
return height;
}
public void setHeight(float height) {
this.height = height;
}
}
PersonHeight personHeight = new PersonHeight(0);
Thread t1 = new Thread(() -> {
personHeight.setHeight(11);
System.out.println(personHeight.getHeight());
});
Thread t2 = new Thread(() -> {
personHeight.setHeight(22);
System.out.println(personHeight.getHeight());
});
t1阔拳、t2共享成員變量personHeight并修改,顯而易見會引發(fā)線程并發(fā)安全(有關線程安全問題請參考:真正理解Java Volatile的妙用类嗤,為此需要使用ThreadLocal糊肠,改造如下:
class PersonHeight {
float height;
public PersonHeight(float height) {
this.height = height;
}
public float getHeight() {
return height;
}
public void setHeight(float height) {
this.height = height;
}
}
ThreadLocal<PersonHeight> threadLocal = new ThreadLocal<>();
Thread t1 = new Thread(() -> {
threadLocal.set(new PersonHeight(11));
System.out.println(threadLocal.get().getHeight());
});
Thread t2 = new Thread(() -> {
threadLocal.set(new PersonHeight(22));
System.out.println(threadLocal.get().getHeight());
});
這樣就沒有線程安全問題了。
以上思想是網上一些帖子對ThreadLocal 使用場景的闡述遗锣,認為ThreadLocal的作用是為了避免線程安全問題货裹。個人認為這例子用來描述使用ThreadLocal的理由并不太恰當。
兩點理由:
1精偿、可以看出使用ThreadLocal時弧圆,實際上是重新生成了新的PersonHeight對象。既然兩個線程訪問的不是同一Person對象笔咽,那么就沒有線程安全問題搔预,沒有線程安全問題,何來的避免線程安全問題呢叶组?(多個線程各自訪問不同的變量拯田,這不叫避免了線程安全問題)。
2扶叉、我們現(xiàn)在的設備勿锅,更多的瓶頸是在內存而非CPU,因此上面例子里解決并發(fā)問題我們可以上鎖來解決枣氧。PersonHeight對象所占空間很小溢十,復制一份對象沒問題,如果對象很大呢达吞,為了線程安全問題张弛,每個線程都新建一份豈不是很浪費內存。
綜上所述,ThreadLocal并不是為了解決線程安全問題而設計的吞鸭。
問題來了寺董,ThreadLocal在什么場景使用?還是從例子開始:
1刻剥、變量在線程之間無需可見共享
class DataPool {
int data[];
public DataPool(int[] data) {
this.data = data;
}
public int[] getData() {
return data;
}
public void setData(int[] data) {
this.data = data;
}
}
private void writeDataPool() {
dataPool.getData()[0] = 3;
}
private int readDataPool(int pos) {
if (dataPool.getData().length <= pos) {
return -1;
}
return dataPool.getData()[pos];
}
DataPool dataPool = new DataPool(new int[5]);
Thread t1 = new Thread(() -> {
writeDataPool();
readDataPool(1);
});
Thread t2 = new Thread(() -> {
writeDataPool();
readDataPool(1);
});
我們本來要設計線程訪問各自私有的數(shù)據池遮咖,它們之間的數(shù)據池毫無關聯(lián)也不需要關聯(lián),只和線程本身有關造虏。如上寫法明顯不符合設計初衷御吞,因此我們無需定義為成員變量,改造如下:
2漓藕、避免過多無用參數(shù)傳遞
private void writeDataPool(DataPool dataPool, int pos) {
if (dataPool == null || dataPool.getData() == null || dataPool.getData().length <= pos)
return;
dataPool.getData()[0] = 3;
}
private int readDataPool(int pos) {
//讀取dataPool pos處的值
return 0;
}
private void performA() {
performB();
}
private void performB() {
performC();
}
private void performC() {
int value = readDataPool(0);
}
Thread t1 = new Thread(() -> {
DataPool dataPool = new DataPool(new int[5]);
writeDataPool(dataPool, 0);
performA();
});
Thread t2 = new Thread(() -> {
DataPool dataPool = new DataPool(new int[5]);
writeDataPool(dataPool, 0);
performA();
});
t1陶珠、t2已經擁有各自的dataPool,各自處理互不影響享钞。線程先往dataPool里寫入數(shù)據揍诽,而后經過performA()->performB()->performC()->readDataPool(int pos)層層調用,最后想讀取dataPool的值栗竖。但是問題來了暑脆,readDataPool(int pos) 能讀取到dataPool變量的值嗎?顯而易見划滋,readDataPool(int pos)沒有任何引用能找到dataPool對象饵筑,因此無法讀取dataPool的值。這時候你可能想到了处坪,沒有dataPool對象根资,我傳進去啊不行嗎?沒錯同窘,是可以傳玄帕,但是回溯整個調用棧就需要每個調用的地方都需要傳dataPool對象,此方法可行嗎想邦?可行裤纹!可取嗎?不可壬ッ弧鹰椒!試想,先不說有多少層的調用就要寫多少傳參呕童,關鍵是中間調用比如performA()漆际、performB()、performC()根本不關心dataPool夺饲,為啥要扣個參數(shù)在它們頭上呢奸汇?
我們再想想施符,怎樣才能讓線程里調用的任何方法都可以訪問到dataPool呢?聰明的你可能想到了成員變量擂找,既然線程里執(zhí)行戳吝,那么可以在Thread類里增加成員變量來存儲dataPool。
成員變量存儲dataPool:
private int readDataPool(int pos) {
//讀取dataPool pos處的值
DataPool dataPool = ((NewThread) Thread.currentThread()).getDataPool();
if (dataPool != null && dataPool.getData().length > pos) {
return dataPool.getData()[pos];
}
return 0;
}
class NewThread extends Thread {
public NewThread(@Nullable Runnable target) {
super(target);
}
private DataPool dataPool;
public DataPool getDataPool() {
return dataPool;
}
public void setDataPool(DataPool dataPool) {
this.dataPool = dataPool;
}
}
NewThread t1 = new NewThread(() -> {
DataPool dataPool = new DataPool(new int[5]);
((NewThread) Thread.currentThread()).setDataPool(dataPool);
writeDataPool(dataPool, 0);
performA();
});
NewThread t2 = new NewThread(() -> {
DataPool dataPool = new DataPool(new int[5]);
((NewThread) Thread.currentThread()).setDataPool(dataPool);
writeDataPool(dataPool, 0);
performA();
});
如上贯涎,NewThread繼承自Thread听哭,并新增DataPool字段,一開始將DataPool對象存儲至dataPool字段柬采,而后在readDataPool(int pos)里獲取欢唾,這樣子就無需層層傳遞參數(shù)了。
既然要做成通用的字段粉捻,就不能直接使用DataPool類型了,將之改為泛型:
class NewThread<T> extends Thread {
public NewThread(@Nullable Runnable target) {
super(target);
}
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
大家注意到了我們t1斑芜、t2分別調用了setData getData
((NewThread) Thread.currentThread()).setData(dataPool);
((NewThread) Thread.currentThread()).getDataPool();
實際上調用方式一模一樣的肩刃,想想是不是可以統(tǒng)一管理一下。
static NewThreadLocal<DataPool> newThreadLocal = new NewThreadLocal<>();
NewThread t1 = new NewThread(() -> {
DataPool dataPool = new DataPool(new int[5]);
newThreadLocal.set(dataPool);
writeDataPool(dataPool, 0);
performA();
});
NewThread t2 = new NewThread(() -> {
DataPool dataPool = new DataPool(new int[5]);
newThreadLocal.set(dataPool);
writeDataPool(dataPool, 0);
performA();
});
static class NewThreadLocal<T> {
public void set(T data) {
((NewThread) Thread.currentThread()).setData(data);
}
public T getData() {
return (T) ((NewThread) Thread.currentThread()).getData();
}
}
為了線程方便調用newThreadLocal變量杏头,將之定義為static類型盈包。
之前我們處理的都是單個數(shù)據類型如DataPool,現(xiàn)在將DataPool分為免費和收費
static NewThreadLocal<FreeDataPool> newThreadLocalFree = new NewThreadLocal<>();
static NewThreadLocal<ChargeDataPool> newThreadLocalCharge = new NewThreadLocal<>();
NewThread t1 = new NewThread(() -> {
FreeDataPool freeDataPool = new FreeDataPool(new int[5]);
ChargeDataPool chargeDataPool = new ChargeDataPool(new int[5]);
newThreadLocalFree.set(freeDataPool);
newThreadLocalCharge.set(chargeDataPool);
writeDataPool(freeDataPool, 0);
writeDataPool(chargeDataPool, 0);
performA();
});
class FreeDataPool extends DataPool {
public FreeDataPool(int[] data) {
super(data);
}
}
class ChargeDataPool extends DataPool {
public ChargeDataPool(int[] data) {
super(data);
}
}
可以看到醇王,freeDataPool變量會覆蓋chargeDataPool呢燥,因為NewThread只有一個data字段。為了一個線程內能夠存儲多個不同類型的變量寓娩,考慮將data字段升級為Map存儲叛氨。當然freeDataPool和chargeDataPool當作Map的value字段,那么key該選什么呢棘伴?key分別選擇newThreadLocalFree和newThreadLocalCharge寞埠,一般我們會用對象的hashcode方法『缚洌可在NewThreadLocal里添加生成key的方法仁连,最終改造如下:
class NewThread<T> extends Thread {
public NewThread(@Nullable Runnable target) {
super(target);
}
private Map<String, T> data;
public Map getData() {
return data;
}
public void setData(Map data) {
this.data = data;
}
}
class NewThreadLocal<T> {
public void set(T data) {
Map<String, T> map = ((NewThread) Thread.currentThread()).getData();
map.put(this.hashCode() + "", data);
}
public T getData() {
Map<String, T> map = ((NewThread) Thread.currentThread()).getData();
return map.get(this.hashCode() + "");
}
}
static NewThreadLocal<FreeDataPool> newThreadLocalFree = new NewThreadLocal<>();
static NewThreadLocal<ChargeDataPool> newThreadLocalCharge = new NewThreadLocal<>();
NewThread t1 = new NewThread(() -> {
FreeDataPool freeDataPool = new FreeDataPool(new int[5]);
ChargeDataPool chargeDataPool = new ChargeDataPool(new int[5]);
newThreadLocalFree.set(freeDataPool);
newThreadLocalCharge.set(chargeDataPool);
writeDataPool(freeDataPool, 0);
writeDataPool(chargeDataPool, 0);
performA();
});
NewThread使用Map存儲newThreadLocalFree和newThreadLocalCharge變量,這樣不管NewThread存儲多少個變量都不會覆蓋阱穗。老規(guī)矩饭冬,還是用圖表示一下:
至此,我們從探究ThreadLocal使用的場景揪阶,一步步摸索到自定義NewThreadLocal解決遇到的問題昌抠。幸運的是,Java JDK工程師已經考慮上述所遇到的問題遣钳,就是本篇的主角ThreadLocal扰魂。NewThreadLocal基本上和ThreadLocal對應麦乞,理解了NewThreadLocal,相信很快看懂ThreadLocal劝评,如果直接分析ThreadLocal可能不太好理解為啥這么設計姐直。新模型/方法的設計一定為了解決某些問題的,只有知其然也知所以然才能指導我們更好理解問題直至解決問題蒋畜。NewThreadLocal和Java ThreadLocal有些差異声畏,為方便理解,對應關系如下:
- NewThreadLocal->ThreadLocal
- NewThread->Thread
- NewThread.data->Thread.threadLocals
- Map->ThreadLocal.ThreadLocalMap
NewThreadLocal和ThreadLocal差異之處:
在于存儲結構的不同姻成,NewThreadLocal使用的是HashMap插龄,而ThreadLocal使用的是自定義的ThreadLocalMap,簡單比較一下兩者(HashMap和ThreadLocalMap異同點):
1科展、HashMap使用key本身的hashcode再進行運算得出均牢,而ThreadLocalMap使用ThreadLocal對象作為key,因此有可能引入內存泄漏問題才睹,為此該key使用WeakReference變量修飾徘跪。
2、兩者都使用Hash算法:除留余數(shù)法
ThreadLocalMap生成 hash的方法:
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
使用static變量nextHashCode琅攘,其類型為AtomicInteger垮庐,每次使用后都會加上固定值HASH_INCREMENT(斐波那契數(shù)列相關,目的使hash值分布更均勻)坞琴,最后將結果更新賦值給nextHashCode哨查。而HashMap使用key的hashcode進行再運算得出。
3剧辐、hash沖突解決方式:HashMap使用鏈地址法寒亥,ThreadLocalMap使用開放地址法-線性探測法。
總結:
TheadLocal應用場景:
變量在線程之間無需可見共享浙于,為線程獨有
變量創(chuàng)建和使用在不同的方法里且不想過度重復書寫形參
ThreadLocal在Android 源碼里的運用
大家還記得上篇文章解析過Handler-Looper护盈,Handler和Looper共享MessageQueue是通過Looper對象作為橋梁的,而Looper對象是以ThreadLocal方式存放在線程里的羞酗。
// sThreadLocal.get() will return null unless you've called prepare().
@UnsupportedAppUsage
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
@UnsupportedAppUsage
private static Looper sMainLooper; // guarded by Looper.class
分三個步驟說一下:
1腐宋、構造并設置Looper對象
開始的時候檀轨,先構造Looper對象胸竞,并設置到ThreadLocal里(實際設置到線程變量里),如下:
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
//ThreadLocal設置looper對象
sThreadLocal.set(new Looper(quitAllowed));
}
2卫枝、構造Handler并關聯(lián)MessageQueue
Looper.java
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
public Handler(@Nullable Callback callback, boolean async) {
if (FIND_POTENTIAL_LEAKS) {
final Class<? extends Handler> klass = getClass();
if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
(klass.getModifiers() & Modifier.STATIC) == 0) {
Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
klass.getCanonicalName());
}
}
//獲取looper對象
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread " + Thread.currentThread()
+ " that has not called Looper.prepare()");
}
//關聯(lián)MessageQueue
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
Looper.myLooper()方法獲取存放在當前線程里的looper對象,如果在第一步中當前線程里調用過prepare設置looper對象沾乘,那么此刻myLooper()就能返回之前設置的looper對象迁央。
3、Looper.java loop()遍歷MessageQueue
public static void loop() {
//獲取當前線程looper對象
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
//獲取MessageQueue
final MessageQueue queue = me.mQueue;
//遍歷MessageQueue獲取message
}
通過以上三個步驟就實現(xiàn)了Android里的消息隊列循環(huán)崇摄∫傥茫可以看出,Looper對象是和線程有關,并且不是線程間共享的,每個線程擁有自己獨立的Looper對象,并且其一次創(chuàng)建互捌,多個不同的地方使用硼讽,因此使用ThreadLocal來持有Looper對象是比較合適的。
在創(chuàng)建主線程的Looper對象時,將之賦予static變量sMainLooper,而子線程的looper對象存放在ThreadLocal。我們判斷是否在主線程里的方法之一:
if (Looper.myLooper() == Looper.getMainLooper()) {
//主線程
}
當然,當前線程可能沒有l(wèi)ooper,說明一定是主線程链快。
ThreadLocal 會有內存泄漏嗎
內存泄漏產生的原因:長生命周期對象持有短生命周期的對象導致短生命周期對象無法被釋放域蜗。典型的例子就是handler泄漏脉执,因為handler作為內部類持有activity引用半夷,而handler被MessageQueue持有,MessageQueue一直都存在(除非handler的發(fā)送的消息都執(zhí)行完畢或者手動移除handler發(fā)送的消息)宾舅。
我們知道ThreadLocal僅僅只是個管理類而已蔬蕊,真正的對象存儲在Thread里们妥。ThreadLocal會被當作ThreadLocalMap的key监婶,而Thread持有ThreadLocalMap,進而間接持有ThreadLocal,正常情況下這就可能有內存泄漏的風險(Thread長周期 ThreadLocal短周期)养渴,對此ThreadLocalMap對此做了預防:
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使用了弱引用。
再者藐唠,我們正常使用的時候,一般會設置ThreadLocal為static,而static又是全局的,就不會存在ThreadLocal泄漏的說法了他去。
但是:Entry的key使用了弱引用,而其value(我們存儲的對象)并沒有使用弱引用行施。當key沒有強引用的時候涯雅,會被回收活逆,此時key=null丽惶,而value并沒有被回收壤圃。
綜上兩點,ThreadLocal變量本身不存在內存泄漏問題冲杀,但是value有內存泄漏風險。
解決方法:使用完畢記得調用ThreadLocal.remove()移除key和value壶辜。
為什么Entry value不設置為弱引用呢悯舟?
還是以Looper.java為例
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
//new Looper(quitAllowed)->Looper looper = new Looper(quitAllowed)
//這里直接new一個Looper對象,最終持有這個對象的引用是ThreadLocalMap里的Entry value
//如果此時value設置為弱引用砸民,當GC發(fā)生時抵怎,由于該Looper對象沒有強引用,因此會被回收岭参。
//而此時Entry key還存在反惕,通過key索引value找不到當初設置的Looper對象。
sThreadLocal.set(new Looper(quitAllowed));
}
對線程狀態(tài)有疑問的同學請移步:Java 線程狀態(tài)