金三銀四跳槽季,BAT美團滴滴java面試大綱(帶答案版)之二:ThreadLocal和Valotile
繼續面試大綱系列文章。
(強烈推薦關注公眾號:pnxsxb ,有更多更及時的學習內容分享,還會不定期有專屬於程序員的好禮相送)也可以長按識別以下二維碼關注:
ThreadLocal和Valotile是兩個比較常見的知識點,雖然簡單,但是能從一定程度上考察一個程序員,對多線程環境下,線程通信和數據安全的認知。閑話少說,進入正題:
一.ThreadLocal
- 問:請談談你對ThreadLocal的理解。
- 分析:在多線程環境下,我們經常遇到這樣的場景:維護一個全局變量。如果要保證變量值的正確性(或者說變量值修改的原子性), 需用什么方式來實現呢?是的,對修改代碼加鎖可以實現,保證了在同一時刻只有一個線程來修改該變量值。辦法當然不止一 種,並發包AtomicXXX一樣能達到這個效果,原理,差不多,無非是通過鎖來實現並發。那么還有沒有其他思路呢?有,ThreadLocal,實現思路可謂是另辟蹊徑。
- 答:每個線程,都會有一個Map(ThreadLocalMap),用來存儲以我們定義的ThreadLocal對象為key,以我們自定義的值為value的 名值對。而這個Map,是來自於我們寫的多線程程序繼承的父線程Thread。以此機制,保證了多線程間該變量值的隔離。
看下源碼,以get()方法為切入口:
1 public T get() { 2 Thread t = Thread.currentThread(); 3 ThreadLocalMap map = getMap(t); 4 if (map != null) { 5 ThreadLocalMap.Entry e = map.getEntry(this); 6 if (e != null) { 7 @SuppressWarnings("unchecked") 8 T result = (T)e.value; 9 return result; 10 } 11 } 12 return setInitialValue(); 13 }
重點是第三行,當前線程作為參數傳入,我們來看下getMap(t)做了什么?
1 ThreadLocalMap getMap(Thread t) { 2 return t.threadLocals; 3 }
是的,拿到當前線程對象的threadLocals對象,我們可以通過方法返回值推斷,是一個ThreadLocalMap類型的對象。那么這個對象在哪定義的呢?繼續看源碼:
1 public class Thread implements Runnable { 2 ...... 3 ThreadLocal.ThreadLocalMap threadLocals = null; 4 ...... 5 }
很明顯,是在Thread類里定義。
4.擴展:內存泄漏問題。
ThreadLocal對象是弱引用。在GC時,會直接回收。這種情況下,Map中的key為null,value值還在,無法得到及時的釋放。目前的策略是在調用get、set、remove等方法時,會啟動回收這些值。但是如果一直沒調用呢?嗯,很容易就導致內存泄漏了。當然,並不能因為此就認為是弱引用導致的內存泄露,而應該是,設計的這個變量存儲機制,導致了泄露。所以在使用的時候,要及時釋放(通過以上描述,你肯定已經想到怎么合理釋放了吧?)
二.Valotile
1.問:請你說下對Valotile的了解,以及使用場景。
2.分析:多線程編程,我們要解決的問題集中在三個方面:
a.原子性,最簡單的例子就是,i++,在多線程環境下,最終的結果是不確定的,為什么?就是因為這么一個++操作,被編譯為指令 后,是多個指令來完成的。那么遇到並發的情況,就會導致彼此“覆蓋”的情況。
b.可見性,通俗解釋就是,在A線程對一個變量做了修改,在B線程中,能正確的讀取到修改后的結果。究其原理,是cpu不是直 接 和系統內存通信,而是把變量讀取到L1,L2等內部的緩存中,也叫作私有的數據工作棧。修改也是在內部緩存中,但是何時 同步到系統內存是不能確定的,有了這個時間差,在並發的時候,就可能會導致,讀到的值,不是最新值。
c.有序性:這里只說指令重排序,虛擬機在把代碼編譯為指令后執行,出於優化的目的,在保證結果不變的情況下,可能會調整指 令的執行順序。
3.答:valotile,能滿足上述的可見性和有序性。但是無法保證原子性。
可見性,是在修改后,強制把對變量的修改同步到系統內存。而其他cpu在讀取自己的內部緩存中的值的時候,發現是valotile修飾 的,會把內部緩存中的值,置為無效,然后從系統內存讀取。
有序性,是通過內存屏障來實現的。所謂的內存屏障,可以理解為,在某些指令中,插入屏障指令,用以確保,在向屏障指令后面 繼續執行的時候,其前面的所有指令已經執行完畢。
4.擴展:在寫單例模式時,我們通常會采用雙層判斷的方式,在最內層:
instance = new Singleton()
其實這也有一個隱含的問題:這句賦值語句,其實是分三步來操作的:
a.為instance分配內存
b.調用Singleto構造函數來初始化變量
c.instance指向上一步初始化的對象
在jvm做了指令重排序優化后,上述步驟b和c不能保證,可能出現,c先執行,但是對象卻沒初始化,這時候其他線程判斷的時候,發現是非null,但是使用的時候,卻沒有具體實例,導致報錯。
所以,我們可以用valotile來修飾instance,避免該問題。
有了以上知識儲備,相信可以應對80%的面試挑戰了。如果還有興趣深入了解,可以留言交流。
歡迎掃描以下二維碼,關注個人公眾號,更及時獲取第一手學習資料: