ThreadLocal
基本在項目開發中基本不會用到, 可是面試官是比較喜歡問這類問題的;因此仍是有必要了解一下該類的功能與原理的.
ThreadLocal
是一個將在多線程中爲每個線程建立單獨的變量副本的類; 當使用ThreadLocal來維護變量時, ThreadLocal
會爲每一個線程建立單獨的變量副本, 避免因多線程操做共享變量而致使的數據不一致的狀況;java
通常來講, ThreadLocal
在實際工業生產中並不常見, 可是在不少框架中使用卻可以解決一些框架問題; 好比Spring中的事務、Spring 中 做用域 Scope
爲 Request的Bean
使用ThreadLocal來解決.面試
一、將須要被多線程訪問的屬性使用ThreadLocal變量來定義; 下面以網上多數舉例的DBConnectionFactory類爲例來舉例sql
import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBConnectionFactory { private static final ThreadLocal<Connection> dbConnectionLocal = new ThreadLocal<Connection>() { @Override protected Connection initialValue() { try { return DriverManager.getConnection("", "", ""); } catch (SQLException e) { e.printStackTrace(); } return null; } }; public Connection getConnection() { return dbConnectionLocal.get(); } }
這樣在Client獲取Connection的時候, 每一個線程獲取到的Connection都是該線程獨有的, 作到Connection的線程隔離; 因此並不存在線程安全問題數組
一、主要是用到了Thread對象中的一個ThreadLocalMap類型的變量threadLocals, 負責存儲當前線程的關於Connection的對象, 以dbConnectionLocal
這個變量爲Key, 以新建的Connection
對象爲Value; 這樣的話, 線程第一次讀取的時候若是不存在就會調用ThreadLocal
的initialValue
方法建立一個Connection對象而且返回;緩存
具體關於爲線程分配變量副本的代碼以下:安全
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(); }
一、首先獲取當前線程對象t
, 而後從線程t
中獲取到ThreadLocalMap
的成員屬性threadLocals
數據結構
二、若是當前線程的threadLocals
已經初始化(即不爲null
) 而且存在以當前ThreadLocal對象爲Key的值, 則直接返回當前線程要獲取的對象(本例中爲Connection);多線程
三、若是當前線程的threadLocals
已經初始化(即不爲null
)可是不存在以當前ThreadLocal對象爲Key的的對象, 那麼從新建立一個Connection對象, 而且添加到當前線程的threadLocals Map中,並返回併發
四、若是當前線程的threadLocals
屬性尚未被初始化, 則從新建立一個ThreadLocalMap對象, 而且建立一個Connection對象並添加到ThreadLocalMap對象中並返回。框架
若是存在則直接返回很好理解, 那麼對於如何初始化的代碼又是怎樣的呢?
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; }
一、首先調用咱們上面寫的重載事後的initialValue
方法, 產生一個Connection對象
二、繼續查看當前線程的threadLocals
是否是空的, 若是ThreadLocalMap已被初始化, 那麼直接將產生的對象添加到ThreadLocalMap中, 若是沒有初始化, 則建立並添加對象到其中;
同時, ThreadLocal還提供了直接操做Thread對象中的threadLocals的方法
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
這樣咱們也能夠不實現initialValue
, 將初始化工做放到DBConnectionFactory
的getConnection
方法中:
public Connection getConnection() { Connection connection = dbConnectionLocal.get(); if (connection == null) { try { connection = DriverManager.getConnection("", "", ""); dbConnectionLocal.set(connection); } catch (SQLException e) { e.printStackTrace(); } } return connection; }
那麼咱們看過代碼以後就很清晰的知道了爲何ThreadLocal可以實現變量的多線程隔離了; 其實就是用了Map的數據結構給當前線程緩存了, 要使用的時候就從本線程的threadLocals對象中獲取就能夠了, key就是當前線程;
固然了在當前線程下獲取當前線程裏面的Map裏面的對象並操做確定沒有線程併發問題了, 固然能作到變量的線程間隔離了;
如今咱們知道了ThreadLocal究竟是什麼了, 又知道了如何使用ThreadLocal以及其基本實現原理了是否是就能夠結束了呢? 其實還有一個問題就是ThreadLocalMap是個什麼對象, 爲何要用這個對象呢?
本質上來說, 它就是一個Map, 可是這個ThreadLocalMap
與咱們平時見到的Map
有點不同
一、它沒有實現Map
接口;
二、它沒有public的方法, 最多有一個default的構造方法, 由於這個ThreadLocalMap的方法僅僅在ThreadLocal類中調用, 屬於靜態內部類
三、ThreadLocalMap的Entry實現繼承了WeakReference<ThreadLocal<?>>
四、該方法僅僅用了一個Entry數組來存儲Key, Value; Entry並非鏈表形式, 而是每一個bucket裏面僅僅放一個Entry;
要了解ThreadLocalMap的實現, 咱們先從入口開始, 就是往該Map中添加一個值:
private void set(ThreadLocal<?> key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. 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)]) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
先進行簡單的分析, 對該代碼表層意思進行解讀:
一、看下當前threadLocal的在數組中的索引位置 好比: `i = 2`, 看 `i = 2` 位置上面的元素(Entry)的`Key`是否等於threadLocal 這個 Key, 若是等於就很好說了, 直接將該位置上面的Entry的Value替換成最新的就能夠了; 二、若是當前位置上面的 Entry 的 Key爲空, 說明ThreadLocal對象已經被回收了, 那麼就調用replaceStaleEntry 三、若是清理完無用條目(ThreadLocal被回收的條目)、而且數組中的數據大小 > 閾值的時候對當前的Table進行從新哈希
因此, 該HashMap是處理衝突檢測的機制是向後移位, 清除過時條目 最終找到合適的位置;
瞭解完Set方法, 後面就是Get方法了:
private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); }
先找到ThreadLocal的索引位置, 若是索引位置處的entry不爲空而且鍵與threadLocal是同一個對象, 則直接返回; 不然去後面的索引位置繼續查找;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ThreadLocalDemo { static class LocalVariable { private Long[] a = new Long[1024 * 1024]; } // (1) final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>()); // (2) final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>(); public static void main(String[] args) throws InterruptedException { // (3) Thread.sleep(5000 * 4); for (int i = 0; i < 50; ++i) { poolExecutor.execute(new Runnable() { public void run() { // (4) localVariable.set(new LocalVariable()); // (5) System.out.println("use local varaible" + localVariable.get()); localVariable.remove(); } }); } // (6) System.out.println("pool execute over"); } }
我在網上找到一個樣例, 若是用線程池來操做ThreadLocal
對象確實會形成內存泄露, 由於對於線程池裏面不會銷燬的線程, 裏面總會存在着<ThreadLocal, LocalVariable>的強引用, 由於final static
修飾的 ThreadLocal
並不會釋放, 而ThreadLocalMap
對於 Key 雖然是弱引用, 可是強引用不會釋放, 弱引用固然也會一直有值, 同時建立的LocalVariable
對象也不會釋放, 就形成了內存泄露; 若是LocalVariable
對象不是一個大對象的話, 其實泄露的並不嚴重, 泄露的內存 = 核心線程數 * LocalVariable對象的大小
;
因此, 爲了不出現內存泄露的狀況, ThreadLocal提供了一個清除線程中對象的方法, 即 remove
, 其實內部實現就是調用 ThreadLocalMap
的remove
方法:
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對應的Entry, 而且清除Entry的Key(ThreadLocal)置空, 隨後清除過時的Entry便可避免內存泄露;