首先,我沒有用過ThreadLocal類,前幾天被問及相關使用場景和實現原理,一臉懵逼,只知道是本地線程,所以特學習一下相關的知識
由於水平所限,歡迎批評指正
什麼是ThreadLocal
引用百度百科的解釋:早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal爲解決多線程程序的併發問題提供了一種新的思路。使用這個工具類可以很簡潔地編寫出優美的多線程程序。
ThreadLocal很容易讓人望文生義,想當然地認爲是一個“本地線程”。其實,ThreadLocal並不是一個Thread,而是Thread的局部變量,也許把它命名爲ThreadLocalVariable更容易讓人理解一些。
可是還是不明白,翻閱資料,覺得這個解釋是比較容易理解的:
如果你創建一個類對象,實現Runnable接口,然後多個Thread對象使用同樣的Runnable對象,全部的線程都共享同樣的屬性。這意味着,如果你在一個線程裏改變一個屬性,全部的線程都會受到這個改變的影響。
BUT
有時,你希望程序裏的各個線程的屬性不會被共享。 Java 併發 API提供了一個很清楚的機制叫本地線程變量。
我們看一個舉個栗子分析一下
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();
}
}
假設有這樣一個數據庫鏈接管理類,這段代碼在單線程中使用是沒有任何問題的,但是如果在多線程中使用呢?很顯然,在多線程中使用會存在線程安全問題:第一,這裏面的2個方法都沒有進行同步,很可能在openConnection方法中會多次創建connect;第二,由於connect是共享變量,那麼必然在調用connect的地方需要使用到同步來保障線程安全,因爲很可能一個線程在使用connect進行數據庫操作,而另外一個線程調用closeConnection關閉鏈接。
BUT
我們真的需要共享connect變量嗎?真的必須保證線程是同步執行的嗎?答案是否定的。
假如每個線程中都有一個connect變量,各個線程之間對connect變量的訪問實際上是沒有依賴關係的,即一個線程不需要關心其他線程是否對這個connect進行了修改的。如果有線程關閉掉了connect的連接,那麼我們正在使用數據庫的其他線程就會無法與數據庫進行訪問,由於多個線程之間沒有依賴關係,那麼我們也不是必須要保證線程同步。這不是我們希望的場景。
到這裏,可能會有朋友想到,既然不需要在線程之間共享這個變量,可以直接這樣處理,在每個需要使用數據庫連接的方法中具體使用時才創建數據庫鏈接,然後在方法調用完畢再釋放這個連接。比如下面這樣:
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進行操作
doSomething();
connectionManager.closeConnection();
}
}
這樣處理確實也沒有任何問題,由於每次都是在方法內部創建的連接,那麼線程之間自然不存在線程安全問題。但是這樣會有一個致命的影響:導致服務器壓力非常大,並且嚴重影響程序執行性能。由於在方法中需要頻繁地開啓和關閉數據庫連接,這樣不僅嚴重影響程序執行效率,還可能導致服務器壓力巨大。
那麼這種情況下使用ThreadLocal是再適合不過的了,因爲ThreadLocal在每個線程中對該變量會創建一個副本,即每個線程內部都會有一個該變量,且在線程內部任何地方都可以使用,線程之間互不影響,這樣一來就不存在線程安全問題,也不會嚴重影響程序執行性能。
但是要注意,雖然ThreadLocal能夠解決上面說的問題,但是由於在每個線程中都創建了副本,所以要考慮它對資源的消耗,比如內存的佔用會比不使用ThreadLocal要大。
深入解析ThreadLocal類
解析.ThreadLocal類,無外乎就是閱讀源碼
我們先看一下ThreadLocal裏面的幾個重要的方法
public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }
get方法
get()方法是用來獲取ThreadLocal在當前線程中保存的變量副本看一下相關源碼:
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();
}
第一句是取得當前線程,然後通過getMap(t)方法獲取到一個map,map的類型爲ThreadLocalMap。然後接着下面獲取到 < key, value>鍵值對,注意這裏獲取鍵值對傳進去的是 this,而不是當前線程t。
如果獲取成功,則返回value值。
如果map爲空,則調用setInitialValue方法返回value。
首先看一下getMap方法中做了什麼:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可能大家沒有想到的是,在getMap中,是調用當期線程t,返回當前線程t中的一個成員變量threadLocals。
那麼我們繼續取Thread類中取看一下成員變量threadLocals是什麼:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
也就是threadLocals 是一個ThreadLocalMap ,那麼ThreadLocalMap 是什麼呢?
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作爲鍵值。
我們再看get方法中的最後一句話,如果map是空則返回setInitialValue();
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;
}
很容易瞭解,就是如果map不爲空,就設置鍵值對,爲空,再創建Map,看一下createMap的實現:
/**
* 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
* @param map the map to store.
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
至此,可能大部分朋友已經明白了ThreadLocal是如何爲每個線程創建變量的副本的:
首先,在每個線程Thread內部有一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個threadLocals就是用來存儲實際的變量副本的,鍵值爲當前ThreadLocal變量,value爲變量副本(即T類型的變量)。
初始時,在Thread裏面,threadLocals爲空,當通過ThreadLocal變量調用get()方法或者set()方法,就會對Thread類中的threadLocals進行初始化,並且以當前ThreadLocal變量爲鍵值,以ThreadLocal要保存的副本變量爲value,存到threadLocals。
然後在當前線程裏面,如果要使用副本變量,就可以通過get方法在threadLocals裏面查找。
- set方法
set()用來設置當前線程中變量的副本
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);
}
我們在使用set方法時,直接set值即可,實際上剛纔我們談到的getMap方法,返回當前線程t中的一個成員變量threadLocals,那麼當獲得的Map不爲空,我們還是需要執行createMap方法,來創建當前線程可用的ThreadLocalMap,如果已經創建好了Map,那麼我們就可以直接對Map進行set操作,同樣set的key爲this。
- remove方法
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
* {@code initialValue} method in the current thread.
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
remove的方法我們可以看到,實際上我們要remove掉的是當前線程ThreadLocalMap 裏的值,我們當然要調用的是ThreadLocalMap 裏面的remove的方法
源碼如下:
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
我們可以看到,先計算出來key在map中的地址,然後再進行遍歷查找,找到對應的entry就clear掉,也就是相當於remove了。
- initialValue 方法
initialValue()是一個protected方法,一般是用來在使用時進行重寫的,它是一個 延遲加載方法
/**
* Returns the current thread's "initial value" for this
* thread-local variable. This method will be invoked the first
* time a thread accesses the variable with the {@link #get}
* method, unless the thread previously invoked the {@link #set}
* method, in which case the {@code initialValue} method will not
* be invoked for the thread. Normally, this method is invoked at
* most once per thread, but it may be invoked again in case of
* subsequent invocations of {@link #remove} followed by {@link #get}.
*
* <p>This implementation simply returns {@code null}; if the
* programmer desires thread-local variables to have an initial
* value other than {@code null}, {@code ThreadLocal} must be
* subclassed, and this method overridden. Typically, an
* anonymous inner class will be used.
*
* @return the initial value for this thread-local
*/
protected T initialValue() {
return null;
}
我們可以看到方法的初始值爲null,一般情況下,我們會對這個方法進行重寫,來獲得一些細節
舉個例子
下面通過一個例子來證明通過ThreadLocal能達到在每個線程中創建變量副本的效果:
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());
}
}
從這段代碼的輸出結果可以看出,在main線程中和thread1線程中,longLocal保存的副本值和stringLocal保存的副本值都不一樣。最後一次在main線程再次打印副本值是爲了證明在main線程中和thread1線程中的副本值確實是不同的。
總結一下:
1)實際的通過ThreadLocal創建的副本是存儲在每個線程自己的threadLocals中的;
2)爲何threadLocals的類型ThreadLocalMap的鍵值爲ThreadLocal對象,因爲每個線程中可有多個threadLocal變量,就像上面代碼中的longLocal和stringLocal;
3)在進行get之前,必須先set,否則會報空指針異常;
如果想在get之前不需要調用set就能正常訪問的話,必須重寫initialValue()方法。
因爲在上面的代碼分析過程中,我們發現如果沒有先set的話,即在map中查找不到對應的存儲,則會通過調用setInitialValue方法返回,而在setInitialValue方法中,有一個語句是T value = initialValue(), 而默認情況下,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的話,運行時會報空指針異常。
我們分析返回值是null沒有問題,空指針異常的問題是由於null需要強制轉換爲long類型是報的錯誤
如果改成下面這段代碼,即重寫了initialValue方法:
ThreadLocal<Long> longLocal= new ThreadLocal<Long>(){;
protected String initialValue() {
return Thread.currentThread().getId();
};
};
就可以直接不用先set而直接調用get了。
ThreadLocal的應用場景
最常見的ThreadLocal使用場景爲 用來解決 數據庫連接、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 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;
}
參考文獻:
https://www.cnblogs.com/dolphin0520/p/3920407.html
https://baike.baidu.com/item/ThreadLocal/4915311?fr=aladdin
http://ifeve.com/thread-management-10/
本文總結了上述文獻的一些內容,增加了對於一些方法的講解,對於已知的描述不清之處進行了修正
最後非常感謝 張孝祥 老師,深入淺出講明白了ThreadLocal本文中的部分代碼也是套用了張孝祥老師的演示代碼。附上視頻鏈接,侵權刪。
https://www.bilibili.com/video/av7592261/