1. ThreadLocal是什麼?
首先,學過操作系統的都應該知道,同一個進程中的線程之間的頭是相互獨立的,數據部分是共享的。現在如果我們想讓每個線程都有自己的數據,從而實現線程數據隔壁,那麼如何實現?ThreadLocal
就是來做這個事情的,通過ThreadLocal
可以給線程設置自己的局部變量,也可取出自己的局部變量。說白了,ThreadLocal就是想在多線程環境下去保證成員變量的安全。
另外,通過ThreadLocal
的字面意思(線程的局部變量)也可以初步瞭解其作用。
注意:本文中說的線程的局部變量是指線程自己的數據,不是指方法中的局部變量。
2. ThreadLocal如何實現對線程局部變量的操作
2.1 分析
首先,我們查看一下ThreadLocal
類中的源碼:
以下是ThreadLocal
類中的部分源碼:
public class ThreadLocal<T> {
ThreadLocal.ThreadLocalMap threadLocals = null;
public T get() {
//得到當前的線程對象
Thread t = Thread.currentThread();
//獲取該線程的屬性:ThreadLocalMap;ThreadLocalMap是用來存放該線程所有的局部變量的容器,是一個map結構
ThreadLocalMap map = getMap(t);
//如果map存在,則將當前ThreadLocal對象作爲key,根據key獲取內容
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
public void set(T value) {
//得到當前的線程對象
Thread t = Thread.currentThread();
//獲取該線程的屬性:ThreadLocalMap
ThreadLocalMap map = getMap(t);
//如果map存在,則以當前ThreadLocal對象爲key,在map中設置key-value
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
//獲取該線程的成員變量:threadLocals
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//ThreadLocalMap類,是ThreadLocal的內部類
static class ThreadLocalMap {
// Entry類,是ThreadLocalMap的內部類
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
...
tab[i] = new Entry(key, value);
...
}
}
}
注意:如果以上源碼看不懂沒關係,接着往下看,我詳細解釋。
public void set(T value) {
//得到當前的線程對象
Thread t = Thread.currentThread();
//獲取該線程的屬性:ThreadLocalMap
ThreadLocalMap map = getMap(t);
//如果map存在,則以當前ThreadLocal對象爲key,從Entry中設置key-value
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
//獲取該線程的屬性:ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
從getMap
這個方法,我們可以得出結論,threadLocals
是線程的一個成員變量。
從set
方法,我們可以知道,threadLocals
是一個ThreadLocalMap
類型的變量,並且以當前的ThreadLocal
對象爲key
,線程要存放的局部變量爲value
存放於map中。
由此可見,線程的成員變量threadLocals
就是用來存放線程的局部變量的容器,是一個map結構。爲線程存放局部變量時,是以當前的ThreadLocal
對象爲key,要存放的局部變量爲value存放數據的。另外,線程的局部變量是有線程對象管理的,而不是交給ThreadLocal管理的,因爲threadLocals
是線程對象的屬性。
現在,我們知道了ThreadLocalMap
是用來存放線程局部變量的容器,那麼我們接下來來了解ThreadLocalMap
:
//ThreadLocalMap類,是ThreadLocal的內部類
static class ThreadLocalMap {
// Entry類,是ThreadLocalMap的內部類
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
...
tab[i] = new Entry(key, value);
...
}
}
}
通過以上代碼可知,ThreadLocalMap
是ThreadLocal
的內部類,Entry類是ThreadLocalMap
的內部類。ThreadLocalMap
的map結構實際上是通過Entry
類型的數組實現的。
2.2 小結
- 每個Thread維護着一個存放線程局部變量的容器:
ThreadLocalMap
,其是一個map結構。 ThreadLocalMap
是ThreadLocal
的內部類,其map結構是用Entry
數組實現的,也就是數據最終存放在Entry
對象中。- 調用
ThreadLoca
l的set()
給線程設置局部變量時,實際上就是往ThreadLocalMap
設置值,key
是ThreadLocal
對象,值是傳遞進來的要設置局部變量。 - 調用
ThreadLocal
的get()
方法時,實際上就是往ThreadLocalMap
獲取值,key是ThreadLocal
對象。 ThreadLocal
本身並不存儲值,它只是作爲一個key來讓線程從ThreadLocalMap
獲取value。
其關係,可以用如下一張圖表示:
3. 舉一個例子幫助理解
比如,我們在實現銀行轉賬的時候,A給B轉500元,具體步驟如下:
1. 從數據庫中讀取A的錢
2. 從數據庫中讀取B的錢
3. A的錢 - 500
4. B的錢 + 500
5. 將A現在的錢寫入數據庫
6. 將B現在的錢寫入數據庫
這6步中操作數據庫時,應該用的是同一個Connection
連接對象,因爲這樣能夠保證這1,2,5,6
操作數據庫時如果有一個沒有操作成功則整個6步都無效,從而保證了不會一方加錢,另一方不減錢的情況。
那麼如何實現這6步中操作數據庫時,應該用的是同一個Connection
連接對象?
其實,我們只需要爲每個線程對象的局部變量中存放同一個Connection
連接對象對象就可以實現。具體代碼如下:
public class DBUtil {
//數據庫連接池
private static BasicDataSource source;
//爲不同的線程管理連接
private static ThreadLocal<Connection> local;
static {
try {
//加載配置文件
Properties properties = new Properties();
//獲取讀取流
InputStream stream = DBUtil.class.getClassLoader().getResourceAsStream("連接池/config.properties");
//從配置文件中讀取數據
properties.load(stream);
//關閉流
stream.close();
//初始化連接池
source = new BasicDataSource();
//設置驅動
source.setDriverClassName(properties.getProperty("driver"));
//設置url
source.setUrl(properties.getProperty("url"));
//設置用戶名
source.setUsername(properties.getProperty("user"));
//設置密碼
source.setPassword(properties.getProperty("pwd"));
//初始化線程本地
local = new ThreadLocal<>();
} catch (IOException e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
if(local.get()!=null){
return local.get();
}else{
//獲取Connection對象
Connection connection = source.getConnection();
//把Connection放進ThreadLocal裏面
local.set(connection);
//返回Connection對象
return connection;
}
}
//關閉數據庫連接
public static void closeConnection() {
//從線程中拿到Connection對象
Connection connection = local.get();
try {
if (connection != null) {
//恢復連接爲自動提交
connection.setAutoCommit(true);
//這裏不是真的把連接關了,只是將該連接歸還給連接池
connection.close();
//既然連接已經歸還給連接池了,ThreadLocal保存的Connction對象也已經沒用了
local.remove();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
4. 內存泄露的問題
首先,簡單介紹一下一些相關術語:
-
內存泄漏:指程序中己動態分配的堆內存由於某種原因程序未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至系統崩潰等嚴重後果。
-
強引用:指如果一個對象=null, 但是該對象在被其他對象使用,則該對象不會被垃圾回收機制回收。
-
弱引用:指如果一個對象=null, 但是該對象在被其他對象使用,則該對象會被垃圾回收機制回收。
看如下一段源碼:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
可以看見,實際存儲類Entry
對ThreadLocal對象是弱引用關係,也就是ThreadLocalMap對於key是弱引用關係。
爲什麼要設置成若引用關係?
你可以這樣想,如果ThreadLocal = null時,ThreadLocalMap中存放以ThreadLocal爲key的鍵值對是否應該刪除,ThreadLocal 是否應該刪除???
答案顯然是都應該刪除。如果是強引用,則都不能刪除,只有在該線程被回收時才都被刪除。那麼至少我們應該把能刪除的刪除了,弱引用關係能夠將ThreadLocal刪除,ThreadLocalMap中存放以ThreadLocal爲key的鍵值對通過其他手段刪除。
弱引用關係會造成,ThreadLocal = null時,ThreadLocal被回收,但是ThreadLocalMap中存放以ThreadLocal爲key的鍵值對沒有被回收,且無法被訪問,這樣就造成了內存泄漏,事實上早期是這樣的,現在這個問題被解決了。
解決的方法就是:在ThreadLocalMap中的set/getEntry方法中,會對key爲null(也即是ThreadLocal爲null)進行判斷,如果爲null的話,那麼是會對value置爲null的。當然,我們也可以手動的通過調用ThreadLocal的remove方法進行釋放!
參考文件:
https://blog.csdn.net/qq_42862882/article/details/89820017
https://www.jianshu.com/p/ee8c9dccc953