ThreadLocal 內部實現、應用場景和內存泄漏html
《Java源碼分析》:ThreadLocal /ThreadLocalMapmysql
ThreadLocal並非用來併發控制訪問一個共同對象,而是爲了給每一個線程分配一個只屬於該線程的變量,顧名思義它是local variable(線程局部變量)。
它的功用很是簡單,就是爲每個使用該變量的線程都提供一個變量值的副本, 是每個線程均可以獨立地改變本身的副本,而不會和其它線程的副本衝突,實現線程間的數據隔離。從線程的角度看,就好像每個線程都徹底擁有該變量。
1、首先每一個線程都有一個私有的ThreadLocalMap的引用map 2、當咱們第一次調用threadLocal.set()/threadLocal.get()方法的時候會對這個map進行初始化。 3、這個ThreadLocalMap就是用來保存(threadLocal,value)這樣的鍵值對的,
即每一個使用threadLocal的線程都是將其做爲鍵而指定的值做爲value保存在這個map中,而map是每一個線程私有的,所以是獨立的,能夠隨意操做。 4,當咱們調用threadLocal.get()方法時,他首先會拿到當前線程的ThreadLocalMap對象map,
因爲這個ThreadLocalMap對象map中保存了(threadLocal,value)的鍵值對,
所以根據map.get(threadLocal)來拿到相應的value值,這樣咱們就能夠隨意來操做這個value,不會影響其餘線程中的value。
set和get方法是ThreadLocal類中最經常使用的兩個方法。
1,set方法實現源碼以下:數組
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
//Thread類裏默認threadLocals爲null class Thread implements Runnable{ ThreadLocal.ThreadLocalMap threadLocals = null; }
static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } }
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
ThreadLocalMap.Entry類繼承了WeakReference(弱引用),若是entry.get()==null,意味着key不在引用,所以在table中的鍵值對就會被去除。 關於ThreadLocalMap咱們須要瞭解的一點是:它是用來保存(threadLocal,value)鍵值對。
當咱們須要使用(threadLocal,value)鍵值對中的value時,只須要使用entry.get(threadLocal)便可得到
ThreadLocalMap是ThreadLocal的一個內部類,ThreadLocalMap的構造方法以下:緩存
//table是ThreadLoaclMap類的實例變量
/** * 初始容量,必須爲2的冪 */ private static final int INITIAL_CAPACITY = 16; /** * Entry表,大小必須爲2的冪 */ private Entry[] table; /** * 表裏entry的個數 */ private int size = 0; /** * 從新分配表大小的閾值,默認爲0 */ private int threshold;
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //求得此ThreadLocal在數組中保存的位置 table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); }
hash衝突解決:如何實現一個線程多個ThreadLocal對象,每個ThreadLocal對象是如何區分的呢?安全
private final int threadLocalHashCode = nextHashCode(); private static AtomicInteger nextHashCode = new AtomicInteger(); private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT);//這個方法應該讓nextHashCode自己也進行了相加。 }
對於每個ThreadLocal對象,都有一個final修飾的int型的threadLocalHashCode不可變屬性, 對於基本數據類型,能夠認爲它在初始化後就不能夠進行修改,因此能夠惟一肯定一個ThreadLocal對象。
可是如何保證兩個同時實例化的ThreadLocal對象有不一樣的threadLocalHashCode屬性:
在ThreadLocal類中,還包含了一個static修飾的AtomicInteger(提供原子操做的Integer類)成員變量(即類變量)
和一個static final修飾的常量(做爲兩個相鄰nextHashCode的差值)。
因爲nextHashCode是類變量,因此每一次調用ThreadLocal類均可以保證nextHashCode被更新到新的值,
而且下一次調用ThreadLocal類這個被更新的值仍然可用,同時AtomicInteger保證了nextHashCode自增的原子性。
ThreadLocalMap類的set方法併發
/** * Set the value associated with key. * * @param key the thread local object * @param value the value to be set */ 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; //首先也是根據key的hash值獲得其在數組中的存儲位置 int i = key.threadLocalHashCode & (len-1); /* 對於第hashcode對應的槽有存儲元素,則說明發生了hash碰撞。 發生hash碰撞的解決方法是:以加一的形式逐漸遍歷整個數組,直到找到key或者是找到一個空位。 */ for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); if (k == key) { //此key原本存在,則更新其值便可。 e.value = value; return; } if (k == null) { //找到一個空位 replaceStaleEntry(key, value, i); return; } } //對於此key的hashcode對應的槽沒有存儲元素,則會直接新建一個對象並存儲在這個位置上。 tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
Thread.currentThread獲得當前線程,若是當前線程存在threadLocals這個變量不爲空,
那麼根據當前的ThreadLocal實例做爲key尋找在map中位置,而後用新的value值來替換舊值。
在ThreadLocal這個類中比較引人注目的應該是ThreadLocal->ThreadLocalMap->Entry這個類。這個類繼承自WeakReference。
2,get方法實現源碼以下:
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(); } 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; }
/* 函數功能:返回當前線程在這個ThreadLocal中的初始值。該方法initialValue()將在第一次用戶調用get方法訪問這個變量時被調用, 除非線程以前調用了set方法這樣就會致使該方法不被被調用。 通常狀況下,這個方法至多被每一個線程調用一次。可是,在調用remove方法以後緊跟着調用了get方法則會又一次的調用此方法。 這裏的實現簡單的返回null;若是程序員指望這個ThreadLocal變量有一個初始值(不是null),則咱們須要在子類(內部類)中重寫這個方法。 */ protected T initialValue() { return null; }
ThreadLocalMap類中getEntry(threadLocal)方法
private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); //根據ThreadLocal的hashcode求出其在數組中存儲的下標 Entry e = table[i]; if (e != null && e.get() == key) //進一步比較確認,e==null或e.get()!=key多是此鍵值對已被垃圾回收 return e; else return getEntryAfterMiss(key, i, e); } //在getEntry方法中在key對應的hash槽中沒有直接找到與此key關聯的鍵值對,則會調用此方法 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null)//已被垃圾回收 expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; }
首先咱們經過Thread.currentThread獲得當前線程,而後獲取當前線程的threadLocals變量,
這個變量就是ThreadLocalMap類型的,若是這個變量map不爲空,再獲取ThreadLocalMap.Entry e,
若是e不爲空,則獲取value值返回,不然在Map中初始化Entry,並返回初始值null。
若是map爲空,則建立並初始化map,並返回初始值null。
注意:Entry繼承若引用類,可是Entry類生成的對象並非被弱引用,而是泛型類ThreadLocal(也就是Entry構造器的key)。
static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); //把ThreadLocal對應的鍵值對在Map中刪除 }
在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,則就刪除。
PS:總結threadlocal
1,每一個ThreadLocal對象,只能爲當前線程存放一個對象。 每一個線程有個map,map有個Entry鍵值對對象,用來存放數據。
經過同一個ThradLocal對象設置的值,那麼全部線程的Entry對象的健都是同樣的都是該ThreadLocal對象。 2,每一個ThreadLocal只能存一個對象,爲何當前線程還要經過map來存對象? 由於經過多個ThreadLocal對象存值,都是存在當前線程的map中,那麼當前線程就會存在多個值. 3,如何使用ThreadLocal 在主線程中建立ThreadLocal對象,而且提供存/取值的方法(最好提供remove方法防止內存泄漏)。業務線程經過該方法調用添加和獲取值。
根據上面Entry方法的源碼,咱們知道ThreadLocalMap是使用ThreadLocal的弱引用做爲Key的
(實線表示強引用,虛線表示弱引用)
ThreadLocalMap使用ThreadLocal的弱引用做爲key,若是一個ThreadLocal沒有外部強引用引用他,那麼系統gc的時候,這個ThreadLocal勢必會被回收, 這樣一來,ThreadLocalMap中就會出現key爲null的Entry,就沒有辦法訪問這些key爲null的Entry的value, 若是當前線程再遲遲不結束的話,這些key爲null的Entry的value就會一直存在一條強引用鏈: Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 永遠沒法回收,形成內存泄露。
ThreadLocalMap設計時的對上面問題的對策:
1,首先從ThreadLocal的直接索引位置(經過ThreadLocal.threadLocalHashCode & (table.length-1)運算獲得)獲取Entry e,若是e不爲null而且key相同則返回e;
2,若是e爲null或者key不一致則向下一個位置查詢,若是下一個位置的key和當前須要查詢的key相等,則返回對應的Entry。
不然,若是key值爲null,則擦除該位置的Entry,並繼續向下一個位置查詢。
在這個過程當中遇到的key爲null的Entry都會被擦除,那麼Entry內的value也就沒有強引用鏈,天然會被回收。
仔細研究代碼能夠發現,set操做也有相似的思想,將key爲null的這些Entry都刪除,防止內存泄露。
可是光這樣仍是不夠的,上面的設計思路依賴一個前提條件:
要調用ThreadLocalMap的getEntry函數或者set函數。
這固然是不可能任何狀況都成立的,因此不少狀況下須要使用者手動調用ThreadLocal的remove函數,手動刪除再也不須要的ThreadLocal,防止內存泄露。
因此JDK建議將ThreadLocal變量定義成private static的,這樣的話ThreadLocal的生命週期就更長,
因爲一直存在ThreadLocal的強引用,因此ThreadLocal也就不會被回收,
也就能保證任什麼時候候都能根據ThreadLocal的弱引用訪問到Entry的value值,而後remove它,防止內存泄露。
在上面提到過,每一個thread中都存在一個map, map的類型是ThreadLocal.ThreadLocalMap. Map中的key爲一個threadlocal實例. 這個Map的確使用了弱引用,不過弱引用只是針對key. 每一個key都弱引用指向threadlocal. 當把threadlocal實例置爲null之後,沒有任何強引用指向threadlocal實例,因此threadlocal將會被gc回收. 可是,咱們的value卻不能回收,由於存在一條從current thread鏈接過來的強引用. 只有當前thread結束之後, current thread就不會存在棧中,強引用斷開, Current Thread, Map, value將所有被GC回收。 因此得出一個結論就是隻要這個線程對象被gc回收,就不會出現內存泄露,但在threadLocal設爲null和線程結束這段時間不會被回收的,就發生了咱們認爲的內存泄露。其實這是一個對概念理解的不一致,也沒什麼好爭論的。最要命的是線程對象不被回收的狀況,這就發生了真正意義上的內存泄露。好比使用線程池的時候,線程結束是不會銷燬的,會再次使用的。就可能出現內存泄露。
jdbc鏈接數據庫,以下所示:
Class.forName("com.mysql.jdbc.Driver"); java.sql.Connection conn = DriverManager.getConnection(jdbcUrl);
注意: 一次Drivermanager.getConnection(jdbcurl)得到只是一個connection,並不能知足高併發狀況。
由於connection不是線程安全的,一個connection對應的是一個事物。
每次得到connection都須要浪費cpu資源和內存資源,是很浪費資源的。因此誕生了數據庫鏈接池。
數據庫鏈接池實現原理以下:
pool.getConnection(),都是先從threadlocal裏面拿的,若是threadlocal裏面有,則用,保證線程裏的多個dao操做,用的是同一個connection,以保證事務。
若是新線程,則將新的connection放在threadlocal裏,再get給到線程。 將connection放進threadlocal裏的,以保證每一個線程從鏈接池中得到的都是線程本身的connection。
Hibernate的數據庫鏈接池源碼實現:
public class ConnectionPool implements IConnectionPool { // 鏈接池配置屬性 private DBbean dbBean; private boolean isActive = false; // 鏈接池活動狀態 private int contActive = 0;// 記錄建立的總的鏈接數 // 空閒鏈接 private List<Connection> freeConnection = new Vector<Connection>(); // 活動鏈接 private List<Connection> activeConnection = new Vector<Connection>(); // 將線程和鏈接綁定,保證事務能統一執行 private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>(); public ConnectionPool(DBbean dbBean) { super(); this.dbBean = dbBean; init(); cheackPool(); } // 初始化 public void init() { try { Class.forName(dbBean.getDriverName()); for (int i = 0; i < dbBean.getInitConnections(); i++) { Connection conn; conn = newConnection(); // 初始化最小鏈接數 if (conn != null) { freeConnection.add(conn); contActive++; } } isActive = true; } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SQLException e) { e.printStackTrace(); } } // 得到當前鏈接 public Connection getCurrentConnecton(){ // 默認線程裏面取 Connection conn = threadLocal.get(); if(!isValid(conn)){ conn = getConnection(); } return conn; } // 得到鏈接 public synchronized Connection getConnection() { Connection conn = null; try { // 判斷是否超過最大鏈接數限制 if(contActive < this.dbBean.getMaxActiveConnections()){ if (freeConnection.size() > 0) { conn = freeConnection.get(0); if (conn != null) { threadLocal.set(conn); } freeConnection.remove(0); } else { conn = newConnection(); } }else{ // 繼續得到鏈接,直到重新得到鏈接 wait(this.dbBean.getConnTimeOut()); conn = getConnection(); } if (isValid(conn)) { activeConnection.add(conn); contActive ++; } } catch (SQLException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } return conn; } // 得到新鏈接 private synchronized Connection newConnection() throws ClassNotFoundException, SQLException { Connection conn = null; if (dbBean != null) { Class.forName(dbBean.getDriverName()); conn = DriverManager.getConnection(dbBean.getUrl(), dbBean.getUserName(), dbBean.getPassword()); } return conn; } // 釋放鏈接 public synchronized void releaseConn(Connection conn) throws SQLException { if (isValid(conn)&& !(freeConnection.size() > dbBean.getMaxConnections())) { freeConnection.add(conn); activeConnection.remove(conn); contActive --; threadLocal.remove(); // 喚醒全部正待等待的線程,去搶鏈接 notifyAll(); } } // 判斷鏈接是否可用 private boolean isValid(Connection conn) { try { if (conn == null || conn.isClosed()) { return false; } } catch (SQLException e) { e.printStackTrace(); } return true; } // 銷燬鏈接池 public synchronized void destroy() { for (Connection conn : freeConnection) { try { if (isValid(conn)) { conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } for (Connection conn : activeConnection) { try { if (isValid(conn)) { conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } isActive = false; contActive = 0; } // 鏈接池狀態 @Override public boolean isActive() { return isActive; } // 定時檢查鏈接池狀況 @Override public void cheackPool() { if(dbBean.isCheakPool()){ new Timer().schedule(new TimerTask() { @Override public void run() { // 1.對線程裏面的鏈接狀態 // 2.鏈接池最小 最大鏈接數 // 3.其餘狀態進行檢查,由於這裏還須要寫幾個線程管理的類,暫時就不添加了 System.out.println("空線池鏈接數:"+freeConnection.size()); System.out.println("活動鏈接數::"+activeConnection.size()); System.out.println("總的鏈接數:"+contActive); } },dbBean.getLazyCheck(),dbBean.getPeriodCheck()); } } }
好比一個方法調用另外一個方法時傳入了8個參數,經過逐層調用到第N個方法,傳入了其中一個參數,
此時最後一個方法須要增長一個參數,第一個方法變成9個參數是天然的,可是這個時候,相關的方法都會受到牽連,使得代碼變得臃腫不堪。
這時候就能夠將要添加的參數設置成線程本地變量,來避免參數傳遞。
上面提到的是ThreadLocal一種亡羊補牢的用途,不過也不是特別推薦使用的方式,
它還有一些相似的方式用來使用,就是在框架級別有不少動態調用,調用過程當中須要知足一些協議,
雖然協議咱們會盡可能的通用,而不少擴展的參數在定義協議時是不容易考慮徹底的以及版本也是隨時在升級的,
可是在框架擴展時也須要知足接口的通用性和向下兼容,而一些擴展的內容咱們就須要ThreadLocal來作方便簡單的支持。
簡單來講,ThreadLocal是將一些複雜的系統擴展變成了簡單定義,使得相關參數牽連的部分變得很是容易。
(PS:共享對象的方法換成局部的)
用SimpleDateFormat這個對象,進行日期格式化。
由於建立這個對象自己很費時的,並且咱們也知道SimpleDateFormat自己不是線程安全的,也不能緩存一個共享的SimpleDateFormat實例,
爲此咱們想到使用ThreadLocal來給每一個線程緩存一個SimpleDateFormat實例,提升性能。
同時由於每一個Servlet會用到不一樣pattern的時間格式化類,因此咱們對應每一種pattern生成了一個ThreadLocal實例。
public interface DateTimeFormat { String DATE_PATTERN = "yyyy-MM-dd"; ThreadLocal<DateFormat> DATE_FORMAT = ThreadLocal.withInitial(() -> { return new SimpleDateFormat("yyyy-MM-dd"); }); String TIME_PATTERN = "HH:mm:ss"; ThreadLocal<DateFormat> TIME_FORMAT = ThreadLocal.withInitial(() -> { return new SimpleDateFormat("HH:mm:ss"); }); String DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; ThreadLocal<DateFormat> DATE_TIME_FORMAT = ThreadLocal.withInitial(() -> { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); }); }
爲何SimpleDateFormat不安全,能夠參考此篇博文:
假如咱們把SimpleDateFormat定義成static成員變量,那麼多個thread之間會共享這個sdf對象, 因此Calendar對象也會共享。
假定線程A和線程B都進入了parse(text, pos) 方法, 線程B執行到calendar.clear()後,線程A執行到calendar.getTime(), 那麼就會有問題。
若是不用static修飾,將SimpleDateFormat定義成局部變量: 每調用一次方法就會建立一個SimpleDateFormat對象,方法結束又要做爲垃圾回收。
加鎖性能較差,每次都要等待鎖釋放後其餘線程才能進入。
那麼最好的辦法就是:使用ThreadLocal: 每一個線程都將擁有本身的SimpleDateFormat對象副本。
附-SimpleDateFormat關鍵源碼:
public class SimpleDateFormat extends DateFormat { public Date parse(String text, ParsePosition pos){ calendar.clear(); // Clears all the time fields // other logic ... Date parsedDate = calendar.getTime(); } } abstract class DateFormat{ // other logic ... protected Calendar calendar; public Date parse(String source) throws ParseException{ ParsePosition pos = new ParsePosition(0); Date result = parse(source, pos); if (pos.index == 0) throw new ParseException("Unparseable date: \"" + source + "\"" , pos.errorIndex); return result; } }