一.對ThreadLocal的理解
二.深入解析ThreadLocal類
三.ThreadLocal的應(yīng)用場景
對ThreadLocal的理解
ThreadLocal是一個本地線程副本變量工具類种柑。主要用于將私有線程和該線程存放的副本對象做一個映射米者,各個線程之間的變量互不干擾怪与,在高并發(fā)場景下,可以實現(xiàn)無狀態(tài)的調(diào)用赶诊,特別適用于各個線程依賴不同的變量值完成操作的場景。
ThreadLoal 變量,線程局部變量篙贸,同一個 ThreadLocal 所包含的對象颊艳,在不同的 Thread 中有不同的副本茅特。這里有幾點需要注意:
1.因為每個 Thread 內(nèi)有自己的實例副本,且該副本只能由當(dāng)前 Thread 使用棋枕。這是也是 ThreadLocal 命名的由來白修。
2.既然每個 Thread 有自己的實例副本,且其它 Thread 不可訪問重斑,那就不存在多線程間共享的問題兵睛。
ThreadLocal 提供了線程本地的實例。它與普通變量的區(qū)別在于绸狐,每個使用該變量的線程都會初始化一個完全獨立的實例副本卤恳。ThreadLocal 變量通常被private static修飾。當(dāng)一個線程結(jié)束時寒矿,它所使用的所有 ThreadLocal 相對的實例副本都可被回收突琳。
總的來說,ThreadLocal 適用于每個線程需要自己獨立的實例且該實例需要在多個方法中被使用符相,也即變量在線程間隔離而在方法或類間共享的場景拆融。
我們先寫一個例子
class ConnectionManager {
private static Connection connect = null;
public static Connection openConnection() {
if(connect == null){
connect = DriverManager.getConnection();
}
return connect;
}
public static void closeConnection() {
if(connect!=null)
connect.close();
}
}
假設(shè)有這樣一個數(shù)據(jù)庫鏈接管理類,這段代碼在單線程中使用是沒有任何問題的啊终,但是如果在多線程中使用呢镜豹?很顯然,在多線程中使用會存在線程安全問題:第一蓝牲,這里面的2個方法都沒有進(jìn)行同步趟脂,很可能在openConnection方法中會多次創(chuàng)建connect;第二例衍,由于connect是共享變量昔期,那么必然在調(diào)用connect的地方需要使用到同步來保障線程安全,因為很可能一個線程在使用connect進(jìn)行數(shù)據(jù)庫操作佛玄,而另外一個線程調(diào)用closeConnection關(guān)閉鏈接硼一。
所以出于線程安全的考慮,必須將這段代碼的兩個方法進(jìn)行同步處理梦抢,并且在調(diào)用connect的地方需要進(jìn)行同步處理般贼。
這樣將會大大影響程序執(zhí)行效率,因為一個線程在使用connect進(jìn)行數(shù)據(jù)庫操作的時候,其他線程只有等待哼蛆。
那么大家來仔細(xì)分析一下這個問題蕊梧,這地方到底需不需要將connect變量進(jìn)行共享?事實上人芽,是不需要的望几。假如每個線程中都有一個connect變量,各個線程之間對connect變量的訪問實際上是沒有依賴關(guān)系的萤厅,即一個線程不需要關(guān)心其他線程是否對這個connect進(jìn)行了修改的橄抹。
到這里,可能會有朋友想到惕味,既然不需要在線程之間共享這個變量楼誓,可以直接這樣處理,在每個需要使用數(shù)據(jù)庫連接的方法中具體使用時才創(chuàng)建數(shù)據(jù)庫鏈接名挥,然后在方法調(diào)用完畢再釋放這個連接疟羹。比如下面這樣:
class ConnectionManager {
private Connection connect = null;
public Connection openConnection() {
if(connect == null){
connect = DriverManager.getConnection();
}
return connect;
}
public void closeConnection() {
if(connect!=null)
connect.close();
}
}
class Dao{
public void insert() {
ConnectionManager connectionManager = new ConnectionManager();
Connection connection = connectionManager.openConnection();
//使用connection進(jìn)行操作
connectionManager.closeConnection();
}
}
這樣處理確實也沒有任何問題,由于每次都是在方法內(nèi)部創(chuàng)建的連接禀倔,那么線程之間自然不存在線程安全問題榄融。由于在方法中需要頻繁地開啟和關(guān)閉數(shù)據(jù)庫連接,這樣會導(dǎo)致服務(wù)器壓力非常大救湖,嚴(yán)重影響程序執(zhí)行性能愧杯。
這種情況下我們可以使用ThreadLocal,因為ThreadLocal在每個線程中對該變量會創(chuàng)建一個副本鞋既,即每個線程內(nèi)部都會有一個該變量力九,且在線程內(nèi)部任何地方都可以使用,線程之間互不影響邑闺,這樣一來就不存在線程安全問題跌前,也不會嚴(yán)重影響程序執(zhí)行性能。
但是要注意陡舅,雖然ThreadLocal能夠解決上面說的問題抵乓,但是由于在每個線程中都創(chuàng)建了副本,所以要考慮它對資源的消耗靶衍,比如內(nèi)存的占用會比不使用ThreadLocal要大臂寝。
下圖為ThreadLocal的內(nèi)部結(jié)構(gòu)圖
從上面的結(jié)構(gòu)圖,我們已經(jīng)窺見ThreadLocal的核心機(jī)制:
- 每個Thread線程內(nèi)部都有一個Map摊灭。
- Map里面存儲線程本地對象(key)和線程的變量副本(value)
- 但是,Thread內(nèi)部的Map是由ThreadLocal維護(hù)的败徊,由ThreadLocal負(fù)責(zé)向map獲取和設(shè)置線程的變量值帚呼。
所以對于不同的線程,每次獲取副本值時,別的線程并不能獲取到當(dāng)前線程的副本值煤杀,形成了副本的隔離眷蜈,互不干擾。
深入解析ThreadLocal類
首先 ThreadLocal 是一個泛型類沈自,保證可以接受任何類型的對象酌儒。
因為一個線程內(nèi)可以存在多個 ThreadLocal 對象,所以其實是 ThreadLocal 內(nèi)部維護(hù)了一個 Map 枯途,這個 Map 不是直接使用的 HashMap 忌怎,而是 ThreadLocal 實現(xiàn)的一個叫做 ThreadLocalMap 的靜態(tài)內(nèi)部類。而我們使用的 get()酪夷、set() 方法其實都是調(diào)用了這個ThreadLocalMap類對應(yīng)的 get()榴啸、set() 方法。
Thread線程內(nèi)部的Map在類中描述如下:
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
我們先了解一下ThreadLocal類提供如下幾個核心方法:
public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }
- get()方法用于獲取當(dāng)前線程的副本變量值晚岭。
- set()方法用于設(shè)置當(dāng)前線程的副本變量值鸥印。
- initialValue()為當(dāng)前線程初始副本變量值。
- remove()方法移除當(dāng)前前程的副本變量值坦报。
先看下get方法的實現(xiàn):
/**
* 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)
return (T)e.value;
}
return setInitialValue();
}
第一句是取得當(dāng)前線程库说,然后通過getMap(t)方法獲取到一個map,map的類型為ThreadLocalMap片择。然后接著下面獲取到<key,value>鍵值對潜的,注意這里獲取鍵值對傳進(jìn)去的是 this,而不是當(dāng)前線程t构回。
如果獲取成功夏块,則返回value值。
如果map為空纤掸,則調(diào)用setInitialValue方法返回value脐供。
我們上面的每一句來仔細(xì)分析:
首先看一下getMap方法中做了什么:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可能大家沒有想到的是,在getMap中借跪,是調(diào)用當(dāng)期線程t政己,返回當(dāng)前線程t中的一個成員變量threadLocals。
那么我們繼續(xù)取Thread類中取看一下成員變量threadLocals是什么:
ThreadLocal.ThreadLocalMap threadLocals = null;
實際上就是一個ThreadLocalMap掏愁,這個類型是ThreadLocal類的一個內(nèi)部類歇由,我們繼續(xù)取看ThreadLocalMap的實現(xiàn):
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;
}
}
可以看到ThreadLocalMap的Entry繼承了WeakReference,并且使用ThreadLocal作為鍵值果港。
然后再繼續(xù)看setInitialValue方法的具體實現(xiàn):
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
如果map不為空沦泌,設(shè)置鍵值對為空,再創(chuàng)建Map辛掠,看一下createMap的實現(xiàn):
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocal為每個線程創(chuàng)建變量的副本的過程:
首先谢谦,在每個線程Thread內(nèi)部有一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals释牺,這個threadLocals就是用來存儲實際的變量副本的,鍵值為當(dāng)前ThreadLocal變量回挽,value為變量副本(即T類型的變量)没咙。
初始時,在Thread里面千劈,threadLocals為空祭刚,當(dāng)通過ThreadLocal變量調(diào)用get()方法或者set()方法,就會對Thread類中的threadLocals進(jìn)行初始化墙牌,并且以當(dāng)前ThreadLocal變量為鍵值涡驮,以ThreadLocal要保存的副本變量為value,存到threadLocals憔古。
然后在當(dāng)前線程里面遮怜,如果要使用副本變量,就可以通過get方法在threadLocals里面查找鸿市。
下面通過一個例子來證明通過ThreadLocal能達(dá)到在每個線程中創(chuàng)建變量副本的效果:
public class Test {
ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
ThreadLocal<String> stringLocal = new ThreadLocal<String>();
public void set() {
longLocal.set(Thread.currentThread().getId());
stringLocal.set(Thread.currentThread().getName());
}
public long getLong() {
return longLocal.get();
}
public String getString() {
return stringLocal.get();
}
public static void main(String[] args) throws InterruptedException {
final Test test = new Test();
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
Thread thread1 = new Thread(){
public void run() {
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
};
};
thread1.start();
thread1.join();
System.out.println(test.getLong());
System.out.println(test.getString());
}
}
輸出結(jié)果如下圖
從這段代碼的輸出結(jié)果可以看出锯梁,在main線程中和thread1線程中,longLocal保存的副本值和stringLocal保存的副本值都不一樣焰情。最后一次在main線程再次打印副本值是為了證明在main線程中和thread1線程中的副本值確實是不同的陌凳。
總結(jié)一下:
1)實際的通過ThreadLocal創(chuàng)建的副本是存儲在每個線程自己的threadLocals中的;
2)為何threadLocals的類型ThreadLocalMap的鍵值為ThreadLocal對象内舟,因為每個線程中可有多個threadLocal變量合敦,就像上面代碼中的longLocal和stringLocal;
3)在進(jìn)行g(shù)et之前验游,必須先set充岛,否則會報空指針異常;
如果想在get之前不需要調(diào)用set就能正常訪問的話耕蝉,必須重寫initialValue()方法崔梗。
因為在上面的代碼分析過程中,我們發(fā)現(xiàn)如果沒有先set的話垒在,即在map中查找不到對應(yīng)的存儲蒜魄,則會通過調(diào)用setInitialValue方法返回i,而在setInitialValue方法中场躯,有一個語句是T value = initialValue()谈为, 而默認(rèn)情況下,initialValue方法返回的是null踢关。
看下面這個例子:
public class Test {
ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
ThreadLocal<String> stringLocal = new ThreadLocal<String>();
public void set() {
longLocal.set(Thread.currentThread().getId());
stringLocal.set(Thread.currentThread().getName());
}
public long getLong() {
return longLocal.get();
}
public String getString() {
return stringLocal.get();
}
public static void main(String[] args) throws InterruptedException {
final Test test = new Test();
System.out.println(test.getLong());
System.out.println(test.getString());
Thread thread1 = new Thread(){
public void run() {
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
};
};
thread1.start();
thread1.join();
System.out.println(test.getLong());
System.out.println(test.getString());
}
}
在main線程中伞鲫,沒有先set,直接get的話签舞,運(yùn)行時會報空指針異常榔昔。
但是如果改成下面這段代碼驹闰,即重寫了initialValue方法:
public class Test {
ThreadLocal<Long> longLocal = new ThreadLocal<Long>(){
protected Long initialValue() {
return Thread.currentThread().getId();
};
};
ThreadLocal<String> stringLocal = new ThreadLocal<String>(){;
protected String initialValue() {
return Thread.currentThread().getName();
};
};
public void set() {
longLocal.set(Thread.currentThread().getId());
stringLocal.set(Thread.currentThread().getName());
}
public long getLong() {
return longLocal.get();
}
public String getString() {
return stringLocal.get();
}
public static void main(String[] args) throws InterruptedException {
final Test test = new Test();
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
Thread thread1 = new Thread(){
public void run() {
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
};
};
thread1.start();
thread1.join();
System.out.println(test.getLong());
System.out.println(test.getString());
}
}
就可以直接不用先set而直接調(diào)用get了。
set()方法
/**
* 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);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
set方法過程:
1.獲取當(dāng)前線程的成員變量map
2.map非空撒会,則重新將ThreadLocal和新的value副本放入到map中。
3.map空师妙,則對線程的成員變量ThreadLocalMap進(jìn)行初始化創(chuàng)建诵肛,并將ThreadLocal和value副本放入map中。
remove()方法
/**
* Removes the current thread's value for this thread-local
* variable. If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim. This may result in multiple invocations of the
* <tt>initialValue</tt> method in the current thread.
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
ThreadLocalMap
ThreadLocalMap是ThreadLocal的內(nèi)部類默穴,沒有實現(xiàn)Map接口怔檩,用獨立的方式實現(xiàn)了Map的功能,其內(nèi)部的Entry也獨立實現(xiàn)蓄诽。
在ThreadLocalMap中薛训,也是用Entry來保存K-V結(jié)構(gòu)數(shù)據(jù)的。但是Entry中key只能是ThreadLocal對象仑氛,這點被Entry的構(gòu)造方法已經(jīng)限定死了乙埃。
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
Entry繼承自WeakReference(弱引用,生命周期只能存活到下次GC前)锯岖,但只有Key是弱引用類型的介袜,Value并非弱引用。
ThreadLocalMap的成員變量:
static class ThreadLocalMap {
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
/**
* The number of entries in the table.
*/
private int size = 0;
/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0
}
Hash沖突怎么解決
和HashMap的最大的不同在于出吹,ThreadLocalMap結(jié)構(gòu)非常簡單遇伞,沒有next引用,也就是說ThreadLocalMap中解決Hash沖突的方式并非鏈表的方式捶牢,而是采用線性探測的方式鸠珠,所謂線性探測,就是根據(jù)初始key的hashcode值確定元素在table數(shù)組中的位置秋麸,如果發(fā)現(xiàn)這個位置上已經(jīng)有其他key值的元素被占用渐排,則利用固定的算法尋找一定步長的下個位置,依次判斷竹勉,直至找到能夠存放的位置飞盆。
ThreadLocalMap解決Hash沖突的方式就是簡單的步長加1或減1,尋找下一個相鄰的位置次乓。
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
顯然ThreadLocalMap采用線性探測的方式解決Hash沖突的效率很低吓歇,如果有大量不同的ThreadLocal對象放入map中時發(fā)送沖突,或者發(fā)生二次沖突票腰,則效率很低城看。
所以這里引出的良好建議是:每個線程只存一個變量,這樣的話所有的線程存放到map中的Key都是相同的ThreadLocal杏慰,如果一個線程要保存多個變量测柠,就需要創(chuàng)建多個ThreadLocal炼鞠,多個ThreadLocal放入Map中時會極大的增加Hash沖突的可能。
ThreadLocalMap的問題
由于ThreadLocalMap的key是弱引用轰胁,而Value是強(qiáng)引用谒主。這就導(dǎo)致了一個問題,ThreadLocal在沒有外部對象強(qiáng)引用時赃阀,發(fā)生GC時弱引用Key會被回收霎肯,而Value不會回收,如果創(chuàng)建ThreadLocal的線程一直持續(xù)運(yùn)行榛斯,那么這個Entry對象中的value就有可能一直得不到回收观游,發(fā)生內(nèi)存泄露。
如何避免泄漏
既然Key是弱引用驮俗,那么我們要做的事懂缕,就是在調(diào)用ThreadLocal的get()、set()方法時完成后再調(diào)用remove方法王凑,將Entry節(jié)點和Map的引用關(guān)系移除搪柑,這樣整個Entry對象在GC Roots分析后就變成不可達(dá)了,下次GC的時候就可以被回收荤崇。
如果使用ThreadLocal的set方法之后拌屏,沒有顯示的調(diào)用remove方法,就有可能發(fā)生內(nèi)存泄露术荤,所以養(yǎng)成良好的編程習(xí)慣十分重要倚喂,使用完ThreadLocal之后,記得調(diào)用remove方法瓣戚。
ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();
try {
threadLocal.set(new Session(1, "Misout的博客"));
// 其它業(yè)務(wù)邏輯
} finally {
threadLocal.remove();
}
三.ThreadLocal的應(yīng)用場景
最常見的ThreadLocal使用場景為 用來解決 數(shù)據(jù)庫連接端圈、Session管理等。
private static ThreadLocal<Connection> connectionHolder
= new ThreadLocal<Connection>() {
public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection() {
return connectionHolder.get();
}
private static final ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();
//獲取Session
public static Session getCurrentSession(){
Session session = threadLocal.get();
//判斷Session是否為空子库,如果為空舱权,將創(chuàng)建一個session,并設(shè)置到本地線程變量中
try {
if(session ==null&&!session.isOpen()){
if(sessionFactory==null){
rbuildSessionFactory();// 創(chuàng)建Hibernate的SessionFactory
}else{
session = sessionFactory.openSession();
}
}
threadLocal.set(session);
} catch (Exception e) {
// TODO: handle exception
}
return session;
}
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;
}
每個線程訪問數(shù)據(jù)庫都應(yīng)當(dāng)是一個獨立的Session會話仑嗅,如果多個線程共享同一個Session會話宴倍,有可能其他線程關(guān)閉連接了,當(dāng)前線程再執(zhí)行提交時就會出現(xiàn)會話已關(guān)閉的異常仓技,導(dǎo)致系統(tǒng)異常鸵贬。此方式能避免線程爭搶Session,提高并發(fā)下的安全性脖捻。
使用ThreadLocal的典型場景正如上面的數(shù)據(jù)庫連接管理阔逼,線程會話管理等場景,只適用于獨立變量副本的情況地沮,如果變量為全局共享的嗜浮,則不適用在高并發(fā)下使用羡亩。
總結(jié)
- 每個ThreadLocal只能保存一個變量副本,如果想要一個線程能夠保存多個副本危融,就需要創(chuàng)建多個ThreadLocal畏铆。
- ThreadLocal內(nèi)部的ThreadLocalMap鍵為弱引用,會有內(nèi)存泄漏的風(fēng)險专挪。
- 適用于無狀態(tài)及志,副本變量獨立后不影響業(yè)務(wù)邏輯的高并發(fā)場景。如果如果業(yè)務(wù)邏輯強(qiáng)依賴于副本變量寨腔,則不適合用ThreadLocal解決,需要另尋解決方案率寡。