前幾天也是蠻尷尬的,在進行電話面試的時候因為一個執行緒題,讓自己白白的失去了一份不錯的工作,是個什麼問題那?那就是執行緒的封閉。
執行緒封閉那麼我們現在需要說的就是什麼是執行緒封閉,因為面試官在問到關於執行緒的問題的時候,順帶在最後問了一句,你知道執行緒封閉麼?
啥?瞬間發呆?啥事執行緒封閉,不懂呀,這東西我沒怎麼聽說過呀,那可咋增,也不能瞎扯呀,只能對他說了一句,不好意思哈,這塊我還真沒有研究過,不是很熟,於是在下面的面試過程中,就流於形式了,也可能一個問題回答不會,確實有點不太好,但是會就是會,不會就是不會,你也不能裝懂不是,於是也只能認倒黴了,但是還得認真學習不是麼?
那麼什麼是執行緒封閉呢?
執行緒封閉
在《Java併發程式設計實戰》中,是這樣解釋的,當我們訪問共享的可變資料的時候,我們通常是需要使用同步的,而不使用同步的話,那麼我們就不能共享收據,如果說僅僅是在單執行緒內訪問資料的話,就不需要同步,這種技術被稱之為執行緒封閉。
也就是說,當我們為了執行緒安全的時候,需要同步是為了保證可變資料共享的時候的安全,而另外的一種方式就是保證可變資料不共享,或者是使資料不可變,也就是說讓可變資料不共享,而我們在面試中被問到的所謂的執行緒封閉,就是在單執行緒中訪問資料,也就是說,讓一個可變資料不被多個執行緒共享從而確保安全性和正確性。
執行緒封閉都有哪些種類ThreadLocal類
首先我們先說第一種,使用ThreadLocal,這個類能夠使執行緒中的某個值和儲存值進行關聯,而且它也提供了一系列的方法,ThreadLocal.get()、ThreadLocal.set()、ThreadLocal.initialValue(),這裡面的所有的方法都會是該變數在記憶體中儲存一個副本,
而這三個方法總的來說,都是對共享變數的一個改變,不論是進行初始化,還是進行賦值和改變,都是對共享變數的修改。
那麼怎麼使用ThreadLocal類來維持執行緒的封閉呢?
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){ //初始化ThreadLocal物件connectionHolder的共享變數為Connection型別的物件 public Connection initialValue(){ return DriverManager.getConnection(DB_URL); }};public static Connection getConnection(){ //使用ThreadLocal物件的get()方法獲取其共享變數 return connectionHolder.get();}
以上程式碼來自於《Java併發程式設計實戰》,那麼我們就來分一下,ThreadLocal這個類,一般的來說就是用於防止對可變的變數進行共享的時候出現不安全的操作所針對的,在單執行緒的程式中我們需要時刻保證資料庫的連線,也就是我們只有這一個 Connection,而JDBC的連結物件它是不安全的,所以,我們把這個JDBC的連線儲存在ThreadLocal中,讓每個執行緒都擁有自己的連線。
這麼說是不是就很好理解了,那麼為什麼ThreadLocal這個類能夠儲存執行緒區域性變數的狀態,使得每次訪問此變數時都能獲得實時的、正確的值呢?這個就得看原始碼了,在這裡也給大家講一點,大家有興趣的也可以自己去看看原始碼裡面是怎麼寫的。
ThreadLocal物件為什麼說通常用於防止對可變的單例變數或全域性變數進行共享說這個的時候我們就一定要去看原始碼,我們都知道ThreadLocal類是JDK的lang包下面的類,那麼來吧
public class ThreadLocal<T> { static class ThreadLocalMap { /** * The entries in this hash map extend WeakReference, using * its main ref field as the key (which is always a * ThreadLocal object). Note that null keys (i.e. entry.get() * == null) mean that the key is no longer referenced, so the * entry can be expunged from table. Such entries are referred to * as "stale entries" in the code that follows. */ static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } }}
有心的讀者們已經看到了,Entry?ThreadLocalMap內建立了Entry陣列,其構造行為和HashMap那真的是有點兄弟的意思了,而剛才也說了它的get,initialValue,和set方法都是為了對共享變數進行操作,就是在這裡,當第一次呼叫get()方法時,會呼叫initialValue()方法,預設返回一個空值。所以在使用ThreadLocal時需要將其子類化並重寫此方法,建立需要關聯的變數,
而初始化的過程,就是建立新的ThreadLocalMap物件,將ThreadLocal物件與變數關聯起來,而我們在Thread的方法中就能找到ThreadLocal和ThreadLocalMap關聯的證明,這裡不給大家去尋找了,大家去原始碼裡面搜一下一定可以看到,位置在180行附近。而這樣我們就不難理解為什麼會說ThreadLocal物件為什麼說通常用於防止對可變的單例變數或全域性變數進行共享了。
使用棧封閉來進行執行緒封閉
而在《Java併發程式設計實戰》當中,說了一句話叫做棧封閉是執行緒封閉的一種特例,在棧封閉中,只能透過區域性變數才能訪問物件。
而在看到這句話的時候,也是琢磨了很久,不是很理解其中的意思,但是因為有了案例的解釋,就瞬間感覺變得很明朗了。
public int loadTheArk(Collection<Animal> candidates) { SortedSet<Animal> animals; int numPairs = 0; Animal candidate = null; // animals被封閉在方法中,不要使它們逸出! animals = new TreeSet<Animal>(new SpeciesGenderComparator()); animals.addAll(candidates); for (Animal a : animals) { if (candidate == null || !candidate.isPotentialMate(a)) candidate = a; else { ark.load(new AnimalPair(candidate, a)); ++numPairs; candidate = null; } } return numPairs;}
以上的程式碼同樣是來自於《Java併發程式設計實戰》當中,而這個案例為什麼一看就能明白其中的意思呢?
我們可以看一下,numPairs這個區域性的基本型別的變數,就是你不管幹什麼,你都無法去破壞這個numPairs,也無法去破壞棧的封閉性,因為你這個區域性變量出不去,只是定義在這loadTheArk的程式中,外邊的任何方法想搞事情,不好意思,完全不存在,所以,我們就明白這種棧的封閉是如何實現執行緒封閉的了。
Ad-hoc執行緒封閉
首先我們先說這個Ad-hoc執行緒封閉是個什麼意思,這忽然多出來個名詞,你不知道他是什麼意思,那麼你肯定是不行滴,其實這就是個單詞的事,Ad-hoc的翻譯是來自拉丁語,這個短語的意思是'特設的、特定目的的(地)、即席的、臨時的、將就的、專案的'。這個短語通常用來形容一些特殊的、不能用於其它方面的的,為一個特定的問題、任務而專門設定的解決方案。這個詞彙須與apriori區分。
其實這Ad-hoc執行緒封閉最簡單的一句話,就是維護執行緒封閉性,讓程式自己負責,就這麼low,一個這麼高大上的詞彙,解釋了半天意思就這麼直白,但是還是得把他原來翻譯的解釋出來。
Ad-hoc執行緒封閉是指,維護執行緒封閉性的職責完全由程式實現來承擔。Ad-hoc執行緒封閉是非常脆弱的,因為沒有任何一種語言特性,例如可見性修飾符或區域性變數,能將物件封閉到目標執行緒上。事實上,對執行緒封閉物件(例如,GUI應用程式中的視覺化元件或資料模型等)的引用通常儲存在公有變數中。
說了半天,就第一句話最重要,而且他它還最脆弱,還不推薦使用,那我們一定是放在最後講解,而最重要的已經在前面講過了。
以後大家在遇到這樣的面試的時候,是不是就能夠不再因為這一個問題而出現問題了呢?