線程封閉
避免并發最簡單的方法就是線程封閉。
即把對象封裝到一個線程裡,隻有這一個線程能看到此對象。那麼這個對象就算不是線程安全的也不會出現任何安全問題。
使用ThreadLocal是實作線程封閉的最好方法。ThreadLocal内部維護了一個Map,Map的key是每個線程的名稱,而Map的值就是我們要封閉的對象。每個線程中的對象都對應着Map中一個值,也就是ThreadLocal利用Map實作了對象的線程封閉。
ThreadLocal是什麼
該類提供了線程局部 (thread-local) 變量。這些變量不同于它們的普通對應物,因為通路某個變量(通過其 get 或 set 方法)的每個線程都有自己的局部變量,它獨立于變量的初始化副本。
ThreadLocal 執行個體通常是類中的 private static 字段,它們希望将狀态與某一個線程(例如,使用者 ID 或事務 ID)相關聯。
一個以ThreadLocal對象為鍵、任意對象為值的存儲結構。
是一個資料結構,有點像HashMap,可以儲存"key : value"鍵值對,但是一個ThreadLocal隻能儲存一個,并且各個線程的資料互不幹擾。
該結構被附帶線上程上,也就是說一個線程可以根據一個ThreadLocal對象查詢到綁定在這個線程上的一個值。
ThreadLocal<String> localName = new ThreadLocal();
localName.set("sss");
String name = localName.get();
線上程A中初始化了一個ThreadLocal對象localName,并set了一個值sss,同時線上程A中通過get可拿到之前設定的值,但是如果線上程B中,拿到的将是一個null
這是如何實作的呢?之前說過,ThreadLocal保證了各個線程的資料互不幹擾
看看set(T value)和get()方法的源碼
傳回目前線程該線程局部變量副本中的值
設定此線程局部變量的目前線程的副本到指定的值,大多數的子類都不需要重寫此方法
Thread#threadLocals
可以發現,每個線程中都有一個ThreadLocalMap
- 執行set時,其值是儲存在目前線程的threadLocals變量
- 執行get時,從目前線程的threadLocals變量擷取
是以線上程A中set的值,對線程B來說得不到的
而且線上程B中重新set的話,也不會影響到線程A中的值,保證了線程之間不會互相幹擾
那ThreadLoalMap究竟是什麼
ThreadLoalMap
從名字上看,可以猜到它類似HashMap,但是在ThreadLocal中,并沒實作Map接口
在ThreadLoalMap中,也是初始化一個大小為16的Entry數組table
Entry節點對象用來儲存每一個key-value鍵值對
這裡的key永遠都是ThreadLocal,通過ThreadLocal的set方法,把ThreadLocal對象自己當做key,放進ThreadLoalMap中
注意,ThreadLoalMap的Entry是繼承WeakReference,和HashMap很大的差別是,Entry中沒有next字段,是以不存在連結清單的情況
hash沖突
沒有連結清單結構,那發生hash沖突了怎麼辦?
先看看ThreadLoalMap中插入一個key-value的實作
每個ThreadLocal對象都有一個hash值threadLocalHashCode
每初始化一個ThreadLocal對象,hash值就增加一個固定大小
在插入過程中,根據ThreadLocal對象的hash值,定位到table中的位置i,過程如下
1、如果目前位置是空的,那麼正好,就初始化一個Entry對象放在位置i上
2、位置i已有對象,如果這個Entry對象的key正好是即将設定的key,那麼覆寫value
3、位置i的對象,和即将設定的key沒關系,那麼隻能找下一個空位置
這樣的話,在get時,也會根據ThreadLocal對象的hash值,定位到table中的位置,然後判斷該位置Entry對象中的key是否和get的key一緻,如果不一緻,就判斷下一個位置
可以發現,set和get如果沖突嚴重的話,效率很低,因為ThreadLoalMap是Thread的一個屬性,是以即使在自己的代碼中控制了設定的元素個數,但還是不能控制其它代碼的行為
記憶體洩露
ThreadLocal可能導緻記憶體洩漏,為什麼?
先看看Entry的實作:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
通過之前的分析已經知道,當使用ThreadLocal儲存一個value時,會在ThreadLocalMap中的數組插入一個Entry對象,按理說key-value都應該以強引用儲存在Entry對象中,但在ThreadLocalMap的實作中,key被儲存到了WeakReference對象中
這就導緻了一個問題,ThreadLocal在沒有外部強引用時,發生GC時會被回收,如果建立ThreadLocal的線程一直持續運作,那麼這個Entry對象中的value就有可能一直得不到回收,發生記憶體洩露。
避免記憶體洩露
既然發現有記憶體洩露的隐患,自然有應對政策,在調用ThreadLocal的get()、set()可能會清除ThreadLocalMap中key為null的Entry對象,這樣對應的value就沒有GC Roots可達了,下次GC的時候就可以被回收,當然如果調用remove方法,肯定會删除對應的Entry對象。
如果使用ThreadLocal的set方法之後,沒有顯示的調用remove方法,就有可能發生記憶體洩露,是以養成良好的程式設計習慣十分重要,使用完ThreadLocal之後,記得調用remove方法。
ThreadLocal<String> localName = new ThreadLocal();
try {
localName.set("sss");
// 其它業務邏輯
} finally {
localName.remove();
}
題外小話
首先,ThreadLocal 不是用來解決共享對象的多線程通路問題的,一般情況下,通過set() 到線程中的對象是該線程自己使用的對象,其他線程是不需要通路的,也通路不到的。各個線程中通路的是不同的對象。
另外,說ThreadLocal使得各線程能夠保持各自獨立的一個對象,并不是通過set()來實作的,而是通過每個線程中的new 對象 的操作來建立的對象,每個線程建立一個,不是什麼對象的拷貝或副本。
通過set()将這個新建立的對象的引用儲存到各線程的自己的一個map中,每個線程都有這樣一個map,執行get()時,各線程從自己的map中取出放進去的對象,是以取出來的是各自自己線程中的對象,ThreadLocal執行個體是作為map的key來使用的。
如果set()進去的東西本來就是多個線程共享的同一個對象,那麼多個線程的get()取得的還是這個共享對象本身,還是有并發通路問題。
下面來看一個hibernate中典型的ThreadLocal的應用
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
可以看到在getSession()方法中,首先判斷目前線程中有沒有放進去session,如果還沒有,那麼通過sessionFactory().openSession()來建立一個session,再将session set到線程中,實際是放到目前線程的ThreadLocalMap這個map中,這時,對于這個session的唯一引用就是目前線程中的那個ThreadLocalMap(下面會講到),而threadSession作為這個值的key,要取得這個session可以通過threadSession.get()來得到,裡面執行的操作實際是先取得目前線程中的ThreadLocalMap,然後将threadSession作為key将對應的值取出。這個session相當于線程的私有變量,而不是public的
顯然,其他線程中是取不到這個session的,他們也隻能取到自己的ThreadLocalMap中的東西。要是session是多個線程共享使用的,那還不亂套了。
試想如果不用ThreadLocal怎麼來實作呢?
可能就要在action中建立session,然後把session一個個傳到service和dao中,這可夠麻煩的。
或者可以自己定義一個靜态的map,将目前thread作為key,建立的session作為值,put到map中,應該也行,這也是一般人的想法,
但事實上,ThreadLocal的實作剛好相反,它是在每個線程中有一個map,而将ThreadLocal執行個體作為key,這樣每個map中的項數很少,而且當線程銷毀時相應的東西也一起銷毀了,不知道除了這些還有什麼其他的好處。
總之,ThreadLocal不是用來解決對象共享通路問題的,而主要是提供了保持對象的方法和避免參數傳遞的友善的對象通路方式。歸納了兩點:
- 每個線程中都有一個自己的ThreadLocalMap類對象,可以将線程自己的對象保持到其中,各管各的,線程可以正确的通路到自己的對象
- 将一個共用的ThreadLocal靜态執行個體作為key,将不同對象的引用儲存到不同線程的ThreadLocalMap中,然後線上程執行的各處通過這個靜态ThreadLocal執行個體的get()方法取得自己線程儲存的那個對象,避免了将這個對象作為參數傳遞的麻煩。
當然如果要把本來線程共享的對象通過set()放到線程中也可以,可以實作避免參數傳遞的通路方式,但是要注意get()到的是那同一個共享對象,并發通路問題要靠其他手段來解決。但一般來說線程共享的對象通過設定為某類的靜态變量就可以實作友善的通路了,似乎沒必要放到線程中
ThreadLocal的應用場合
我覺得最适合的是按線程多執行個體(每個線程對應一個執行個體)的對象的通路,并且這個對象很多地方都要用到。
可以看到ThreadLocal類中的變量隻有這3個int型:
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode =
new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
而作為ThreadLocal執行個體的變量隻有 threadLocalHashCode
nextHashCode 和HASH_INCREMENT 是ThreadLocal類的靜态變量
實際上HASH_INCREMENT是一個常量,表示了連續配置設定的兩個ThreadLocal執行個體的threadLocalHashCode值的增量,而nextHashCode 表示了即将配置設定的下一個ThreadLocal執行個體的threadLocalHashCode 的值。
可以來看一下建立一個ThreadLocal執行個體即new ThreadLocal()時做了哪些操作,構造函數ThreadLocal()裡什麼操作都沒有,唯一的操作是這句
private final int threadLocalHashCode = nextHashCode();
那麼nextHashCode()做了什麼呢
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
就是将ThreadLocal類的下一個hashCode值即nextHashCode的值賦給執行個體的threadLocalHashCode,然後nextHashCode的值增加HASH_INCREMENT這個值。
是以ThreadLocal執行個體的變量隻有這個threadLocalHashCode,而且是final的,用來區分不同的ThreadLocal執行個體,ThreadLocal類主要是作為工具類來使用,那麼set()進去的對象是放在哪兒的呢?
看一下上面的set()方法,兩句合并一下成為
ThreadLocalMap map = Thread.currentThread().threadLocals;
這個ThreadLocalMap 類是ThreadLocal中定義的内部類,但是它的執行個體卻用在Thread類中:
public class Thread implements Runnable {
......
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
......
}
再看這句:
if (map != null)
map.set(this, value);
也就是将該ThreadLocal執行個體作為key,要保持的對象作為值,設定到目前線程的ThreadLocalMap 中,get()方法同樣大家看了代碼也就明白了,ThreadLocalMap 類的代碼太多了,我就不帖了,自己去看源碼吧。