ThreadLocal
-
- 通過閱讀本遍你将擷取的知識
- ThreadLocal
- ThreadLocal實作
- ThreadLocalMap
- ThreadLocalMap記憶體洩漏
通過閱讀本遍你将擷取的知識
- ThreadLocal 使用方法
- ThreadLocal 适合使用的場景
- ThreadLocal實作方法與原理
- ThreadLocalMap實作方法與原理
- Thread如何存儲ThreadLocalMap
- ThreadLocalMap記憶體洩漏原因與避坑方法
ThreadLocal被解釋為線程本地變量,生命周期與和它綁定的線程相同,在新建立一個線程時會初始化一個ThreadLocal執行個體,線程銷毀時與會被一同銷毀,線上程内部隻能通路到本線程自己的ThreadLocal執行個體變量。
下面通過一個例子來了解ThreadLocal
public static class ConnectionManger{
private static Connection connection;
public static Connection openConnection() throws SQLException {
if(connection==null){
connection=DriverManager.getConnection("");
}
return connection;
}
public static void close() throws SQLException {
if(connection!=null){
connection.close();
connection=null;
}
}
}
public void demo() throws SQLException {
Connection connection = ConnectionManger.openConnection();
connection.close();
}
ConnectionManger 類為一個管理資料庫連接配接的類,在内部有一個共享變量
connection
。
單線程的應用環境下調用demo方法不會出現什麼問題,運作也正常。
多線程環境下就不好使了,調用demo方法的結果将會不可控,可能什麼出現多個線程使用現一個
Connection
對象,一個關閉連接配接後其它的出現異常。
解決多線程中的問題可以在ConnectionManager的方法中添加同步塊,通過同步塊保證多線程環境下的線程安全,但ConnectionManager将會成為多線程環境下通路資料庫能力上的瓶頸,所有的資料讀寫都将串行化執行。
解決性能瓶頸的辦法是将
ConnectionManager
也多線程化,将它做為一個連接配接存儲池(資料庫連接配接池),通過動态配置池的大小來控制資料讀寫能力。那如何解決業務上多線程使用
Connection
執行個體的問題呢?
ThreadLocal
主角登場,通過上面問題的分析,每一個業務線程在需要使用資料庫連接配接時,通過調用**ConnectionManger.openConnection()方法擷取一個連接配接,在業務線程中通過connection執行個體完成業務操作,再通過ConnectionManger.close()**關閉連接配接;業務之前是互相隔離的,每一個業務配置設定一個
connection
就可以解決串行化的問題。
每個線程都有一個ThreadLocal執行個體,将共享的
connection
執行個體存儲在ThreadLocal,線上程中執行的業務隻需要擷取ThreadLocal中的connection執行個體完成資料讀寫即可,下面看下優化後的ConnectionManager對象:
public void demo() throws SQLException {
Connection connection = ConnectionManger.openConnection();
connection.close();
}
public static class ConnectionManger {
static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
//模拟連接配接池擷取連接配接
private static Connection openNewConnection() {
try {
return DriverManager.getConnection("");
} catch (SQLException ignore) {
}
return null;
}
public static Connection openConnection() throws SQLException {
threadLocal.set(ConnectionManger.openNewConnection());
return threadLocal.get();
}
public static void close() throws SQLException {
threadLocal.get().close();
threadLocal.remove();
}
}
在ConnectionManager對象中引用
ThreadLocal<Connection>
變量類型存儲目前線程中使用的Connection執行個體。
通過優化解決了多線程中線程對資源競争的問題,通過添加同步塊導緻的性能問題。
你好奇ThreadLocal怎麼實作的嗎?我好奇
ThreadLocal實作
ThreadLocal執行個體供外部使用的三個方法分别是:
ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
threadLocal.get();//擷取值
threadLocal.set(<Object>);//設定值
threadLocal.remove();//移除值
get()
用于擷取存儲在目前線程中值,
set()
用于将值存儲在目前線程中,
remove()
用于移除目前線程中的值。
下面來簡單過下各個方法是怎麼實作的:
public T get() {
//擷取目前線程
Thread t = Thread.currentThread();
//擷取線程中的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//擷取Map中的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//沒有map時進行初始化
return setInitialValue();
}
public void set(T value) {
//擷取目前線程
Thread t = Thread.currentThread();
//擷取線程中的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//将值存儲在Map
map.set(this, value);
} else {
//新建立一個Map
createMap(t, value);
}
}
public void remove() {
//擷取目前線程的Map
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
//将值從Map中移除
m.remove(this);
}
}
//擷取線程的Map
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
從上面的源碼中可以得到幾個關鍵知識點:
- Thread使用ThreadLocalMap管理引用的值,ThreadLocalMap是ThreadLocal内部類
- ThreadLocalMap執行個體存儲線上程的
變量中threadLocals
- ThreadLocal的
set
get
執行個體操作的是目前線程中存儲在threadLocals變量中的值remove
- Thread中的threadLocals可能為null,為null時
get
時會進行初始化set
ThreadLocalMap
要完全搞清楚ThreadLocal的底層原理,ThreadLocalMap的實作肯定是要搞清楚的。
在内部和Map的思路比較像,但存儲池改為了數組,Entry對象繼承自WeakReference弱引用類型,和Map類型相似點在于有擴容操作,ThreadLocalMap不繼承于Map類型,隻在思想上相同。
可能有小夥伴要問了,為什麼不直接用Map類型來存儲呢,在存儲資料量比較多時查找性能優于數組;這裡解釋下沒有用Map類型的原因于一般每個線程中存儲在ThreadLocalMap中的資料不會特别多,在這樣的情況下使用數組的性能就要優于Map類型了,查找時不用計算Hash值,資料量大于擴容也比較簡單。
ThreadLocalMap記憶體洩漏
ThreadLocalMap使用内部Entry做為存儲資料的結構,先來看下實作代碼:
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執行個體做為Key,将值做為value變量,ThreadLocalMap的存儲池中将直接存儲Entry執行個體,從Entry類的結構上可以看出繼承了
WeakReference
類型,在建構方法中對TheadLocal執行個體的引用值調用
super(k)
,這樣做的結果是Entry類型中的key對ThreadLocal的值引用為弱引用類型;
在JVM的GC回收中,如果一個值隻是被弱引用了,那麼将在GC回收時對這個引用進行回收。
這又怎樣能導緻記憶體洩漏呢?
根據弱引用的特性,一個值隻被弱引用時會被GC回收,那一起來分析下下面的示例代碼:
public static class ConnectionManger {
private static Connection openNewConnection() {
try {
return DriverManager.getConnection("");
} catch (SQLException ignore) {
}
return null;
}
public static Connection openConnection() throws SQLException {
ThreadLocal<Connection> local = ThreadLocal.withInitial(ConnectionManger::openNewConnection);
return local.get();
}
}
設上面的代碼運作在多線程環境,且線程使用線程池管理,線程池沒有停步且在任務隊列中還有等待執行的任務。
在
openConnection
中使用
ThreadLocal.withInitial
建立了一個ThreadLocal執行個體,随後使用
get
方法傳回值。
在這個示例的
openConnection
方法中沒有對ThreadLocal執行個體做強引用,在方法執行完成後局部變量将會被回收,随後也沒有調用
ThreadLocal
的執行個體方法
remove
将引用的值移除掉。
現在我們來分析下記憶體中JVM進行GC後的情況:
- 使用
建立TheadLocal執行個體後再使用ThreadLocal.withInitial
方法擷取時會将初始值存入線程對應的ThreadLocalMap中,在TheadLocalMap中會使用Entry類型将ThreadLocal類型值做為key,将Connection類型值做為valueget
- JVM開始執行GC對記憶體進行回收,回收時發現Key是弱引用,在其它位址沒有再引用(沒有強引用),JVM對Key的值進行回收
在JVM執行完GC後key被成功回收,但value引用的值并沒有被回收,這時ThreadLocalMap中的Entry的Key将變為null,而value則沒有變,因為沒有執行
remove
操作,這條記錄将會一直存在于ThreadLocalMap中直到線程被銷毀後導緻線程引用的ThreadLocalMap執行個體被回收才會将洩漏的記憶體進行回收。
精彩推薦:
包郵贈書!《阿裡巴巴Java開發手冊 第二版》
知識點:Java sychronized 内部鎖實作原理
知識點: Java FutureTask 使用詳解
Java ClassLoader詳解雙親委派的實作原理