面試大廠一定離不開的——ThreadLocal,它的實現原理你知道嗎?

使用場景

假設我們有一個數據庫連接管理類:

<code>class ConnectionManager {
private static Connection connect = null;
private static String url = System.getProperty("URL");

public static Connection openConnection() {
if(connect == null){
try {
connect = DriverManager.getConnection(url);
} catch (SQLException e) {
e.printStackTrace();
}
}
return connect;
}

public static void closeConnection() {
if(connect!=null) {
try {
connect.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
/<code>

如果這個類被用在多線程環境內,則會存在線程安全問題,那麼可以對這兩個方法添加synchronized關鍵字進行同步處理,不過這樣會大大降低程序的性能,也可以將connection變成局部變量:

<code>class ConnectionManager {
private Connection connect = null;

public Connection openConnection(String url) {
if(connect == null){
try {
connect = DriverManager.getConnection(url);
} catch (SQLException e) {
e.printStackTrace();
}
}
return connect;
}


public void closeConnection() {
if(connect!=null) {
try {
connect.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}

class ConnectionManagerTest {
private String url = System.getProperty("URL");

public void insert() {
ConnectionManager connectionManager = new ConnectionManager();
Connection connection = connectionManager.openConnection(this.url);
//使用connection進行操作
connectionManager.closeConnection();
}
public void update() {
ConnectionManager connectionManager = new ConnectionManager();
Connection connection = connectionManager.openConnection(this.url);
//使用connection進行操作
connectionManager.closeConnection();
}
}
/<code>

每個CURD方法都創建新的數據庫連接會造成數據庫的很大壓力,這裡可以有兩種解決方案:

  1. 使用連接池管理連接,既不是每次都創建、銷燬連接,而是從一個連接池裡借出可用的連接,用完將其歸還。
  2. 可以看到,這裡connection的建立最好是這樣的:每個線程希望有自己獨立的連接來避免同步問題,在線程內部希望共用同一個連接來降低數據庫的壓力,那麼使用ThreadLocal來管理數據庫連接就是最好的選擇了。它為每個線程維護了一個自己的連接,並且可以在線程內共享。
<code>class ConnectionManager {
private static String url = System.getProperty("URL");
private static ThreadLocalconnectionHolder = ThreadLocal.withInitial(() -> {
try {
return DriverManager.getConnection(url);
} catch (SQLException e) {
e.printStackTrace();
}
return null;
});

public static Connection openConnection() {
return connectionHolder.get();
}

public static void closeConnection() {
Connection connect = connectionHolder.get();
if(connect!=null) {
try {
connect.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}/<code>

另外還可以用到其他需要每個線程管理一份自己的資源副本的地方:An Introduction to ThreadLocal in Java

實現原理

這裡面涉及到三種對象的映射:Thread-ThreadLocal對象-ThreadLocal中存的具體內容,既然是每個線程都會有一個資源副本,那麼這個從ThreadLocal對象到存儲內容的映射自然就會存在Thread對象裡:

<code>ThreadLocal.ThreadLocalMap threadLocals = null;/<code>

而ThreadLocal類只是提供了訪問這個Map的接口:

<code>public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
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 map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}/<code>

這個ThreadLocalMap是ThreadLocal的內部類,實現了一個類似HashMap的功能,其內部維護了一個Entry數組,下標就是通過ThreadLocal對象的threadLocalHashCode計算得來。這個Entry繼承自WeakReference,實現對key,也就是ThreadLocal的弱引用:

<code>static class Entry extends WeakReference<threadlocal>    /** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
/<threadlocal>/<code>

內存模型圖如下:

面試大廠一定離不開的——ThreadLocal,它的實現原理你知道嗎?

當ThreadLocal Ref出棧後,由於ThreadLocalMap中Entry對ThreadLocal只是弱引用,所以ThreadLocal對象會被回收,Entry的key會變成null,然後在每次get/set/remove ThreadLocalMap中的值的時候,會自動清理key為null的value,這樣value也能被回收了。

注意:如果ThreadLocal Ref一直沒有出棧(例如上面的connectionHolder,通常我們需要保證ThreadLocal為單例且全局可訪問,所以設為static),具有跟Thread相同的生命週期,那麼這裡的虛引用便形同虛設了,所以使用完後記得調用ThreadLocal.remove將其對應的value清除。

另外,由於ThreadLocalMap中只對ThreadLocal是弱引用,對value是強引用,如果ThreadLocal因為沒有其他強引用而被回收,之後也沒有調用過get/set,那麼就會產生內存洩露,

在使用線程池時,線程會被複用,那麼裡面保存的ThreadLocalMap同樣也會被複用,會造成線程之間的資源沒有被隔離,所以在線程歸還回線程池時要記得調用remove方法。

hash衝突

上面提到ThreadLocalMap是自己實現的類似HashMap的功能,當出現Hash衝突(通過兩個key對象的hash值計算得到同一個數組下標)時,它沒有采用鏈表模式,而是採用的線性探測的方法,既當發生衝突後,就線性查找數組中空閒的位置。

當數組較大時,這個性能會很差,所以建議儘量控制ThreadLocal的數量。

關注我,後續更多幹貨奉上!!!


分享到:


相關文章: