日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

分析Threadlocal内部实现原理,并解决Threadlocal的ThreadLocalMap的hash冲突与内存泄露

發布時間:2023/12/8 编程问答 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 分析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 等等)。

總而言之:

  • ThreadLocal是一種變量類型,我們稱之為“線程局部變量”。
  • 每個線程訪問這種變量的時候,都會創建該變量的副本,這個變量副本為線程私有
  • ThreadLocal類型的變量,一般用private static加以修飾。
  • 例如,下面的例子中這個類為每個線程生成唯一標識。一個線程的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 鏈
  • HashMap里面沒有出現hash沖突時沒有形成單鏈表時,hashmap查找元素很快,get()方法能夠直接定位到元素
  • 但是出現單鏈表后,單 個bucket 里存儲的不是一個 Entry,而是一個 Entry 鏈,系統只能必須按順序遍歷每個 Entry,直到找到想搜索的 Entry 為止。
    • 如果恰好要搜索的 Entry ,位于該 Entry 鏈的最末端(該 Entry 是最早放入該 bucket 中),那系統必須循環到最后才能找到該元素。

    當創建 HashMap 時,有一個默認的負載因子(load factor),其默認值為 0.75,這是時間和空間成本上一種折衷:

    • 增大負載因子,可以減少 Hash 表(就是那個 Entry 數組)所占用的內存空間,但會增加查詢數據的時間開銷,而查詢是最頻繁的的操作(HashMap 的 get() 與 put() 方法都要用到查詢);
    • 減小負載因子會提高數據查詢的性能,但會增加 Hash 表所占用的內存空間。

    解決Threadlocal的hashmap的hash沖突

    Threadlocal如何2層kv的map

    每個線程都各自有一張獨立的散列表,以ThreadLocal對象作為散列表的keyset方法中的值作為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); }

    實現步驟:

  • 得到 key
  • 計算得 hashValue
  • 若不沖突,則直接填入數組
  • 若沖突,則使 hashValue++ ,也就是往后找,直到找到第一個 data[hashValue] 為空的情況,則填入。若到了尾部可循環到前面。
  • 顯然ThreadLocalMap采用線性探測的方式,解決Hash沖突的效率很低,如果有大量不同的ThreadLocal對象放入map中時發送沖突,或者發生二次沖突,則效率很低。

    所以這里引出的良好建議是:

    每個線程只存一個變量,這樣所有的線程,存放到map中的Key,都是相同的ThreadLocal,如果一個線程,要保存多個變量,就需要創建多個ThreadLocal,多個ThreadLocal放入Map中時,會極大的增加Hash沖突的可能

    總結

    以上是生活随笔為你收集整理的分析Threadlocal内部实现原理,并解决Threadlocal的ThreadLocalMap的hash冲突与内存泄露的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。