高併發第六彈:執行緒封閉
當訪問共享的可變資料時,通常需要使用同步。一種避免使用同步的方式就是不共享資料。如果僅在單執行緒內訪問資料,就不需要同步。這種技術被稱為執行緒封閉。
它其實就是把物件封裝到一個執行緒裡,只有一個執行緒能看到這個物件,那麼這個物件就算不是執行緒安全的,也不會出現任何執行緒安全方面的問題。
二 執行緒封閉技術有一個常見的應用:
資料庫連線對應jdbc的Connection物件,Connection物件在實現的時候並沒有對執行緒安全做太多的處理,jdbc的規範裡也沒有要求Connection物件必須是執行緒安全的。 實際在伺服器應用程式中,執行緒從連線池獲取了一個Connection物件,使用完再把Connection物件返回給連線池,由於大多數請求都是由單執行緒採用同步的方式來處理的,並且在Connection物件返回之前,連線池不會將它分配給其他執行緒。因此這種連線管理模式處理請求時隱含的將Connection物件封閉線上程裡面,這樣我們使用的connection物件雖然本身不是執行緒安全的,但是它通過執行緒封閉也做到了執行緒安全。
實際實現的方法也就3種
1. ad-hoc(點對點)執行緒封閉
這是完全靠實現者控制的執行緒封閉,他的執行緒封閉完全靠實現者實現。Ad-hoc執行緒封閉非常脆弱,沒有任何一種語言特效能將物件封閉到目標執行緒上。
2 .棧封閉
棧封閉是我們程式設計當中遇到的最多的執行緒封閉。什麼是棧封閉呢?簡單的說就是區域性變數。多個執行緒訪問一個方法,此方法中的區域性變數都會被拷貝一分兒到執行緒棧中。所以區域性變數是不被多個執行緒所共享的,也就不會出現併發問題。所以能用區域性變數就別用全域性的變數,全域性變數容易引起併發問題。
public class StackLocalDemo {public int returnNum(int num) { // 物件被封閉在方法中,不要使它們逸出 List<Integer>demoList =new ArrayList<>(); for (int i = 0; i < num; i++) { demoList.add(i); } return demoList.size(); } }
3 . ThreadLocal執行緒封閉:
它是一個特別好的封閉方法,其實ThreadLocal內部維護了一個map,map的key是每個執行緒的名稱,而map的value就是我們要封閉的物件。ThreadLocal提供了get、set、remove方法,每個操作都是基於當前執行緒的,所以它是執行緒安全的。
/** * Sets the current thread's copy of this thread-local variable * to the specified value. Most subclasses will have no need to * override this method, relying solely on the {@link #initialValue} * method to set the values of thread-locals. * * @param value the value to be stored in the current thread's copy of * this thread-local. */ public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } //get方法都與當前執行緒繫結 /** * Returns the value in the current thread's copy of this * thread-local variable. If the variable has no value for the * current thread, it is first initialized to the value returned * by an invocation of the {@link #initialValue} method. * * @return the current thread's value of this thread-local */ public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
其實也就是 是一個引數有了巨大的傳遞性(這其實也是一個大坑). 只要是同一個執行緒在同一個ThreadLocal裡面存放了資料,在其他任何地方都能取出來.
例如: 生命週期與執行緒生命週期相同:
因此,ThreadLocal的一個非常大的“坑”就是當使用不當時,導致使用者不知道它的作用域範圍。大家可能覺得執行緒結束後ThreadLocal應該就回收了。假設執行緒真的登出了確實是這種,可是事實有可能並不是如此。比如線上程池中對執行緒管理都是採用執行緒複用的方法(Web容器通常也會採用執行緒池)。線上程池中執行緒非常難結束甚至於永遠不會結束。這將意味著執行緒持續的時間將不可預測,甚至與JVM的生命週期一致。
ThreadLocal使用的一般步驟:
1、在多執行緒的類(如ThreadDemo類)中。建立一個ThreadLocal物件threadXxx,用來儲存執行緒間須要隔離處理的物件xxx。
2、在ThreadDemo類中。建立一個獲取要隔離訪問的資料的方法getXxx(),在方法中推斷,若ThreadLocal物件為null時候,應該new()一個隔離訪問型別的物件,並強制轉換為要應用的型別。
3、在ThreadDemo類的run()方法中。通過getXxx()方法獲取要操作的資料。這樣能夠保證每一個執行緒相應一個數據物件,在不論什麼時刻都操作的是這個物件。
與Synchonized的對照:
ThreadLocal和Synchonized都用於解決多執行緒併發訪問。可是ThreadLocal與synchronized有本質的差別。synchronized是利用鎖的機制,使變數或程式碼塊在某一時該僅僅能被一個執行緒訪問。而ThreadLocal為每個執行緒都提供了變數的副本,使得每個執行緒在某一時間訪問到的並非同一個物件,這樣就隔離了多個執行緒對資料的資料共享。而Synchronized卻正好相反,它用於在多個執行緒間通訊時可以獲得資料共享。
Synchronized用於執行緒間的資料共享,而ThreadLocal則用於執行緒間的資料隔離。