Android進階知識:ThreadLocal

尋夢新聞LINE@每日推播熱門推薦文章,趣聞不漏接❤️

加入LINE好友

Android進階知識:ThreadLocal 時尚 第1張

Linux編程

點擊右側關注,免費入門到精通!

作者:胖宅老鼠https://juejin.im/post/5ca715dc51882543b33748e1

1、ThreadLocal是什麼?

ThreadLocal是一個線程內部數據存儲類,通過他可以在指定的線程中存儲數據。存儲後,只能在指定的線程中獲取到存儲的數據,對其他線程來說無法獲取到數據。

2、ThreadLocal的使用場景

日常使用場景不多,當某些數據是以線程為作用域並且不同線程具有不同的數據副本的時候,可以考慮使用ThreadLocal。

Android源碼的Lopper、ActivityThread以及AMS中都用到了ThreadLocal。

3、ThreadLocal的使用示例

publicclassThreadLocalActivityextendsAppCompatActivity{privateThreadLocal<String>name=newThreadLocal<>();@OverrideprotectedvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_thread_local);name.set("小明");Log.d("ThreadLocalActivity","Thread:"+Thread.currentThread().getName()+"name:"+name.get());newThread("thread1"){@Overridepublicvoidrun(){name.set("小紅");Log.d("ThreadLocalActivity","Thread:"+Thread.currentThread().getName()+"name:"+name.get());}}.start();newThread("thread2"){@Overridepublicvoidrun(){Log.d("ThreadLocalActivity","Thread:"+Thread.currentThread().getName()+"name:"+name.get());}}.start();}}

運行結果:

D/ThreadLocalActivity:Thread:mainname:小明D/ThreadLocalActivity:Thread:thread1name:小紅D/ThreadLocalActivity:Thread:thread2name:null

可以看到雖然訪問的是同一個ThreadLocal對象,但是獲取到的值卻是不一樣的。

4、ThreadLocal的源碼閱讀

那麼為什麼會造成這樣的結果呢?這就需要去看看ThreadLocal的源碼做到,這里的源碼版本為API 28。主要看它的get和set方法。

set方法:

publicvoidset(Tvalue){Threadt=Thread.currentThread();ThreadLocalMapmap=getMap(t);if(map!=null)map.set(this,value);elsecreateMap(t,value);}

set方法中首先獲取了當前線程對象,然後通過getMap方法傳入當前線程t獲取到一個ThreadLocalMap,接下來判斷這個map是否為空,不為空就直接將當前ThreadLocal作為key,set方法中傳入要保存的值最為value,存放到map中;如果map為空就調用createMap方法創建一個map並同樣將當前ThreadLocal和要保存的值作為key和value加入到map中。

接下先看getMap方法:

ThreadLocalMapgetMap(Threadt){returnt.threadLocals;}

getMap方法比較簡單,就是返回從傳入的當前線程對象的成員變量threadLocals。

接著是createMap方法:

voidcreateMap(Threadt,TfirstValue){t.threadLocals=newThreadLocalMap(this,firstValue);}

createMap方法也很簡單就是new了一個ThreadLocalMap並賦給當前線程對象t中的threadLocals。

原來這個Map是存放在Thread類中的。於是進入Thread類中查看。

Thread.java第188-190行:

/*ThreadLocalvaluespertainingtothisthread.Thismapismaintained*bytheThreadLocalclass.*/ThreadLocal.ThreadLocalMapthreadLocals=null;

根據這里的註釋可以得知,每個線程Thread中都有一個ThreadLocalMap類型的threadLocals成員變量來保存數據,通過ThreadLocal類來進行維護。這樣看來我們每次在不同線程調用ThreadLocal的set方法set的數據是存在不同線程的ThreadLocalMap中的,就像註釋說的ThreadLocal只是起了個維護ThreadLocalMap的功能。想到是get方法同樣也是到不同線程的ThreadLocalMap去取數據。

get方法:

publicTget(){Threadt=Thread.currentThread();ThreadLocalMapmap=getMap(t);if(map!=null){ThreadLocalMap.Entrye=map.getEntry(this);if(e!=null){@SuppressWarnings("unchecked")Tresult=(T)e.value;returnresult;}}returnsetInitialValue();}

果然,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方法:

privateTsetInitialValue(){Tvalue=initialValue();Threadt=Thread.currentThread();ThreadLocalMapmap=getMap(t);if(map!=null)map.set(this,value);elsecreateMap(t,value);returnvalue;}protectedTinitialValue(){returnnull;}

setInitialValue方法中首先調用initialValue方法初始化了一個空value,之後的操作和set方法相同,將這個空的value加入到當前線程的ThreadLocalMap中去,ThreadLocalMap為空就創建個Map,最後返回這個空值。

至此,ThreadLocal的get、set方法就都看過了,也理解了ThreadLocal可以在多個線程中操作而互不干擾的原因。但是ThreadLocal還有一個要注意的地方就是ThreadLocal使用不當會造成內存泄漏。

5、ThreadLocal內存泄漏的原因

內存泄漏的根本原因是當一個對象已經不需要再使用本該被回收時,另外一個正在使用的對象持有它的引用從而導致它不能被回收,導致本該被回收的對象不能被回收而停留在堆內存中。那麼ThreadLocal中是在哪里發生的呢?這就要看到ThreadLocalMap中存儲數據的內部類Entry。

staticclassEntryextendsWeakReference<ThreadLocal<?>>{/**ThevalueassociatedwiththisThreadLocal.*/Objectvalue;Entry(ThreadLocal<?>k,Objectv){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()方法來清除。

推薦↓↓↓

Android進階知識:ThreadLocal 時尚 第3張

👉16個技術公眾號】都在這里!

涵蓋:工程師大咖、源碼共讀、工程師共讀、數據結構與算法、黑客技術和網路安全、大數據科技、編程前端、Java、Python、Web編程開發、Android、iOS開發、Linux、數據庫研發、幽默工程師等。

Android進階知識:ThreadLocal 時尚 第4張

萬水千山總是情,點個 「好看」 行不行

About 尋夢園
尋夢園是台灣最大的聊天室及交友社群網站。 致力於發展能夠讓會員們彼此互動、盡情分享自我的平台。 擁有數百間不同的聊天室 ,讓您隨時隨地都能找到志同道合的好友!