分析Threadlocal内部实现原理,并解决Threadlocal的ThreadLocalMap的hash冲突与内存泄露
前言
ThreadLocal 的經典使用場景是數據庫連接、?session 管理、多線程等……
比如在Spring中,發揮著巨大的作用,在管理Request作用域中的Bean、事務管理、任務調度、AOP等模塊都不同程度使用了ThreadLocal 。
Spring中絕大部分Bean,都可以聲明成Singleton作用域,采用ThreadLocal進行封裝,因此有狀態的Bean,就能夠以singleton的方式,在多線程中正常工作。
知道Threadlocal怎么用,但是不知道為什么要這樣用?底層原理是什么?Threadlocal發生hashmap的hash沖突,怎么辦?
threadlocal是什么?
ThreadLocal提供線程局部變量。
//get()方法是用來獲取ThreadLocal在當前線程中保存的變量副本 public T get() { } //set()用來設置當前線程中變量的副本 public void set(T value) { } //remove()用來移除當前線程中變量的副本 public void remove() { } //initialValue()是一個protected方法,一般是用來在使用時進行重寫的,它是一個延遲加載方法 protected T initialValue(){ }這些變量與普通的變量不同之處在于,每個訪問這種變量的線程(通過它的get或set方法)都有自己的、獨立初始化的變量副本。
ThreadLocal實例,通常是希望將狀態關聯到一個線程的類的私有靜態字段(比如,user ID 或者 Transaction ID 等等)。
總而言之:
例如,下面的例子中這個類為每個線程生成唯一標識。一個線程的id是它第一次調用ThreadId.get()方法指定的。
package com.azdebugit.threadlocal;public class ThreadLocalExsample {private static ThreadLocal<Long> longLocal = new ThreadLocal<>();public void set() {longLocal.set(Thread.currentThread().getId());}public long getLong() {return longLocal.get();}public static void main(String[] args) {ThreadLocalExsample test = new ThreadLocalExsample();//注意:沒有set之前,直接get,報null異常了test.set();System.out.println("-------threadLocal value-------" + test.getLong());longLocal.remove();} }ThreadLocal的應用場景
注意:使用ThreadLocal時,先進行get之前,必須先set,否則會報空指針異常
數據庫連接
@Component public class ConnectionHolderUtil {private static DataSource dataSource;private static final Logger log = LoggerFactory.getLogger(ConnectionHolderUtil.class);@Autowiredpublic void setDataSource(DataSource dataSource) {ConnectionHolderUtil.dataSource = dataSource;}private static ThreadLocal<ConnectionHolder> connectionHolderThreadLocal = new ThreadLocal<>();/** * 獲取數據庫連接 * @return Connection */public static ConnectionHolder getConnectionHolder(boolean isNew){ConnectionHolder connectionHolder = connectionHolderThreadLocal.get();//如果有連接,并不需要生成新的直接返回if(connectionHolder != null && !isNew){return connectionHolder;}try {//獲取新連接Connection connection = dataSource.getConnection();//關閉自動提交connection.setAutoCommit(false);connectionHolder = new ConnectionHolder(connection);connectionHolderThreadLocal.set(connectionHolder);//綁定連接TransactionSynchronizationManager.bindResource(dataSource,connectionHolder);return connectionHolder;} catch (SQLException e) {log.error("數據庫連接獲取失敗",e);return null;}}/** * 提交事務 */public static void commit(){ConnectionHolder connectionHolder = connectionHolderThreadLocal.get();if(connectionHolder == null){return;}try {connectionHolder.getConnection().commit();} catch (SQLException e) {log.error("提交失敗",e);}}/** * 事務回滾 */public static void rollback(){ConnectionHolder connectionHolder = connectionHolderThreadLocal.get();if(connectionHolder == null){return;}try {connectionHolder.getConnection().rollback();} catch (SQLException e) {log.error("回滾失敗",e);}}/** * 關閉連接 */public static void close(){ConnectionHolder connectionHolder = connectionHolderThreadLocal.get();if(connectionHolder == null){return;}Connection connection = connectionHolder.getConnection();try {connection.close();} catch (SQLException e) {log.error("數據庫連接關閉失敗",e);}}/** * 恢復掛起的事務 */public static void resume(Object susPend){TransactionSynchronizationManager.unbindResource(dataSource);TransactionSynchronizationManager.bindResource(dataSource,susPend);connectionHolderThreadLocal.set((ConnectionHolder) susPend);}/** * 掛起當前事務 */public static Object hangTrasaction(){return TransactionSynchronizationManager.unbindResource(dataSource);}/** * 判斷當前連接是否已經關閉 * @return */public static boolean isClose(){if(connectionHolderThreadLocal.get() == null){return true;}try {return connectionHolderThreadLocal.get().getConnection().isClosed();} catch (SQLException e) {log.error("獲取連接狀態失敗");}return true;} }Session管理
@SuppressWarnings("unchecked") public class UserSession { private static final ThreadLocal SESSION_MAP = new ThreadLocal(); protected UserSession() { } public static Object get(String attribute) { Map map = (Map) SESSION_MAP.get(); return map.get(attribute); } public static <T> T get(String attribute, Class<T> clazz) { return (T) get(attribute); } public static void set(String attribute, Object value) { Map map = (Map) SESSION_MAP.get(); if (map == null) { map = new HashMap(); SESSION_MAP.set(map); } map.put(attribute, value); } }多線程
package com.azdebugit.threadlocal;import java.util.concurrent.atomic.AtomicInteger;public class ThreadLocalExsampl {/*** 創建了一個MyRunnable實例,并將該實例作為參數傳遞給兩個線程。兩個線程分別執行run()方法,* 并且都在ThreadLocal實例上保存了不同的值。如果它們訪問的不是ThreadLocal對象并且調用的set()方法被同步了,* 則第二個線程會覆蓋掉第一個線程設置的值。但是,由于它們訪問的是一個ThreadLocal對象,* 因此這兩個線程都無法看到對方保存的值。也就是說,它們存取的是兩個不同的值。*/public static class MyRunnable implements Runnable {/*** 例化了一個ThreadLocal對象。我們只需要實例化對象一次,并且也不需要知道它是被哪個線程實例化。* 雖然所有的線程都能訪問到這個ThreadLocal實例,但是每個線程卻只能訪問到自己通過調用ThreadLocal的* set()方法設置的值。即使是兩個不同的線程在同一個ThreadLocal對象上設置了不同的值,* 他們仍然無法訪問到對方的值。*/private static ThreadLocal threadLocal = new ThreadLocal();@Overridepublic void run() {//一旦創建了一個ThreadLocal變量,你可以通過如下代碼設置某個需要保存的值AtomicInteger atomicInteger = new AtomicInteger();int threadLo = (int) (Math.random() * 100D);System.out.println("-------"+atomicInteger.incrementAndGet()+"-------" + threadLo);threadLocal.set(threadLo);try {Thread.sleep(2000);} catch (InterruptedException e) {}//可以通過下面方法讀取保存在ThreadLocal變量中的值System.out.println("-------"+atomicInteger.incrementAndGet()+"-------"+threadLocal.get());threadLocal.remove();}}public static void main(String[] args) {MyRunnable sharedRunnableInstance = new MyRunnable();for (int i = 0; i < 5; i++) {Thread thread1 = new Thread(sharedRunnableInstance);Thread thread2 = new Thread(sharedRunnableInstance);thread1.start();thread2.start();}} }hashmap的hash沖突
hash沖突--源碼分析
HashMap 采用一種所謂的“Hash 算法”來決定每個元素的存儲位置。
當程序執行 map.put(String,Obect)方法 時,系統將調用String的 hashCode() 方法,得到其 hashCode 值(每個 Java 對象都有 hashCode() 方法,都可通過該方法獲得它的 hashCode 值)。
得到這個對象的 hashCode 值之后,系統會根據該 hashCode 值來決定該元素的存儲位置。源碼如下:
public V put(K key, V value) {if (key == null)return putForNullKey(value);int hash = hash(key.hashCode());int i = indexFor(hash, table.length);for (Entry<K,V> e = table[i]; e != null; e = e.next) {Object k;//判斷當前確定的索引位置是否存在相同hashcode和相同key的元素,如果存在相同的hashcode和相同的key的元素,那么新值覆蓋原來的舊值,并返回舊值。//如果存在相同的hashcode,那么他們確定的索引位置就相同,這時判斷他們的key是否相同,如果不相同,這時就是產生了hash沖突。//Hash沖突后,那么HashMap的單個bucket里存儲的不是一個 Entry,而是一個 Entry 鏈。//系統只能必須按順序遍歷每個 Entry,直到找到想搜索的 Entry 為止——如果恰好要搜索的 Entry 位于該 Entry 鏈的最末端(該 Entry 是最早放入該 bucket 中),//那系統必須循環到最后才能找到該元素。if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {V oldValue = e.value;e.value = value;return oldValue;}}modCount++;addEntry(hash, key, value, i);return null; }
當系統決定存儲 HashMap 中的 key-value 對時,完全沒有考慮 Entry 中的 value,僅僅只是根據 key 來計算,并決定每個 Entry 的存儲位置。這也說明了前面的結論:我們完全可以把 Map 集合中的 value 當成 key 的附屬,當系統決定了 key 的存儲位置之后,value 隨之保存在那里即可。
鏈式地址法--解決散列值的沖突
Hashmap里面的bucket,出現了單鏈表的形式,散列表要解決的一個問題,就是散列值的沖突問題,通常是兩種方法:鏈表地址法和開放地址法。
- 鏈表法,就是將相同hash值的對象,組織成一個鏈表,放在hash值對應的槽位;
- 開放地址法,是通過一個探測算法,當某個槽位已經被占據的情況下,繼續查找下一個可以使用的 槽位。
java.util.HashMap采用的鏈表法的方式,鏈表是單向鏈表。形成單鏈表的核心代碼如下:
void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex];table[bucketIndex] = new Entry<K,V>(hash, key, value, e);if (size++ >= threshold)resize(2 * table.length); }上面方法的代碼很簡單,但其中包含了一個設計:系統總是將新添加的 Entry 對象,放入 table 數組的 bucketIndex 索引處。
- 如果 bucketIndex 索引處,已經有了一個 Entry 對象,那新添加的 Entry 對象,指向原有的 Entry 對象(產生一個 Entry 鏈)。
- 如果 bucketIndex 索引處沒有 Entry 對象,也就是上面程序代碼的 e 變量是 null,也就是新放入的 Entry 對象指向 null,也就是沒有產生 Entry 鏈。
- 如果恰好要搜索的 Entry ,位于該 Entry 鏈的最末端(該 Entry 是最早放入該 bucket 中),那系統必須循環到最后才能找到該元素。
當創建 HashMap 時,有一個默認的負載因子(load factor),其默認值為 0.75,這是時間和空間成本上一種折衷:
- 增大負載因子,可以減少 Hash 表(就是那個 Entry 數組)所占用的內存空間,但會增加查詢數據的時間開銷,而查詢是最頻繁的的操作(HashMap 的 get() 與 put() 方法都要用到查詢);
- 減小負載因子會提高數據查詢的性能,但會增加 Hash 表所占用的內存空間。
解決Threadlocal的hashmap的hash沖突
Threadlocal如何2層kv的map
每個線程都各自有一張獨立的散列表,以ThreadLocal對象作為散列表的key,set方法中的值作為value(第一次調用get方法時,以initialValue方法的返回值作為value)。
如上圖,可以ThreadLocal類用兩層HashMap的kv,進行對象存儲。
外面的HashMap的Key是ThreadID,Value是內層的ThreadLocalMap的維護的Entry(ThreadLocal<?> k, Object v)數組。
內層的HashMap的Key是當前ThreadLocal對象,Value是當前ThreadLocal的值。
所以在Threadlocal中,一個線程中,可能會擁有多個ThreadLocal成員變量,所以內層ThreadLocalMap是為了保存同一個線程中的不同ThreadLocal變量。
ThreadLocal造成的內存泄露和相應解決辦法
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類繼承了WeakRefrence類,所以一個條目,就是一個弱引用類型的對象(要搞清楚,持有weakRefrence對象的引用是個強引用),那么這個weakRefrence對象,保存了誰的弱引用呢?
我們看到構造函數中有個supe(k),k是ThreadLocal類型對象,super表示是調用父類(weakRefrence)的構造函數,所以說一個entry對象中,存儲了ThreadLocal對象的弱引用和這個ThreadLocal對應的value對象的強引用。
那Entry中為什么保存的是key的弱引用呢?
其實這是為了最大程度上減少內存泄露,副作用是同時減少哈希表中的沖突。
當ThreadLocal對象被回收時,對應entry中的key就自動變成null(entry對象本身不為null)。
線程池中的線程是重復使用的,意味著這個線程的ThreadLocalMap對象也是重復使用的,如果我們不手動調用remove方法,那么后面的線程,就有可能獲取到上個線程遺留下來的value值,造成bug。
ThreadLocal-hash沖突及解決方案--線性探測
ThreadLocal對于不同的線程,每次獲取副本值時,別的線程并不能獲取到當前線程的副本值,形成了副本的隔離,互不干擾。
ThreadLocalMap是ThreadLocal的內部類,沒有實現Map接口,用獨立的方式實現了Map的功能,其內部的Entry也獨立實現。
Entry便是ThreadLocalMap里定義的節點,它繼承了WeakReference類,定義了一個類型為Object的value,用于存放塞到ThreadLocal里的值。
在ThreadLocalMap中,也是用Entry來保存K-V結構數據的。但是Entry中key,只能是ThreadLocal對象,這點被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繼承自WeakReference(弱引用,生命周期只能存活到下次GC前),但只有Key是弱引用類型的,Value并非弱引用。
和HashMap的最大的不同在于,ThreadLocalMap結構非常簡單,沒有next引用,也就是說ThreadLocalMap中解決Hash沖突的方式,并非鏈表的方式,而是采用線性探測的方式(開放地址法)。
所謂線性探測,就是根據初始key的hashcode值,確定元素在table數組中的位置,如果發現這個位置上,已經有其他key值的元素被占用,則利用固定的算法,尋找一定步長的下個位置,依次判斷,直至找到能夠存放的位置。
核心:由于ThreadLocalMap使用線性探測法,來解決散列沖突,所以實際上Entry[]數組在程序邏輯上,是作為一個環形存在的。
ThreadLocalMap解決Hash沖突的方式,就是簡單的步長加1或減1,尋找下一個相鄰的位置。
線性探測法:直接使用數組來存儲數據。可以想象成一個停車問題。若當前車位已經有車,則你就繼續往前開,直到找到下一個為空的車位。
/*** Increment i modulo len.*/ private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0); } /*** Decrement i modulo len.*/ private static int prevIndex(int i, int len) {return ((i - 1 >= 0) ? i - 1 : len - 1); }實現步驟:
顯然ThreadLocalMap采用線性探測的方式,解決Hash沖突的效率很低,如果有大量不同的ThreadLocal對象放入map中時發送沖突,或者發生二次沖突,則效率很低。
所以這里引出的良好建議是:
每個線程只存一個變量,這樣所有的線程,存放到map中的Key,都是相同的ThreadLocal,如果一個線程,要保存多個變量,就需要創建多個ThreadLocal,多個ThreadLocal放入Map中時,會極大的增加Hash沖突的可能。
總結
以上是生活随笔為你收集整理的分析Threadlocal内部实现原理,并解决Threadlocal的ThreadLocalMap的hash冲突与内存泄露的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: js对数组的删除操作
- 下一篇: js对象新增方法object.assig