Java線程本地變量導(dǎo)致的緩存問題解決方法
一、前言
前些時(shí)間看別人寫的一段關(guān)于鎖的(對(duì)象緩存+線程本地變量)的一段代碼,這段代碼大致描述了這么一個(gè)功能:
外部傳入一個(gè)key,需要根據(jù)這個(gè)key去全局變量里面找是否存在,如有有則表示有人對(duì)這個(gè)key加鎖了,往下就不執(zhí)行具體業(yè)務(wù)代碼,同時(shí),同時(shí)哦 還要判斷這個(gè)key是不是當(dāng)前線程持有的,如果不是當(dāng)前線程持有的也不能往下執(zhí)行業(yè)務(wù)代碼~
然后哦 還要在業(yè)務(wù)代碼執(zhí)行完成后釋放這個(gè)key鎖,也就是要從 ThreadLocal 里面移除這個(gè)key。
當(dāng)然需求不僅于此,就是業(yè)務(wù)的特殊性需要 ThreadLocal 同時(shí)持有多個(gè)不同的key,這就表明 ThreadLocal 的泛型肯定是個(gè)List或Set。
然后再說下代碼,為了演示問題代碼寫的比較簡略,以下我再一一說明可能存在的問題??
二、基本邏輯
功能大致包含兩個(gè)函數(shù):
lock : 主要是查找公共緩存還有線程本地變量是否包含傳入的指定key,若無則嘗試寫入全局變量及 ThreadLocal 并返回true以示獲取到鎖
release : 業(yè)務(wù)邏輯處理完成后調(diào)用此,此函數(shù)內(nèi)主要是做全局緩存以及 ThreadLocal 內(nèi)的key的移除并返回狀態(tài)(true/false)
contains : 公共方法,供以上兩個(gè)方法使用,邏輯:判斷全局變量或 ThreadLocal 里面有否有指定的key,此方法用 private 修飾
好了,準(zhǔn)備看代碼 ??
代碼如下:
public class CacheObjectLock { // 全局對(duì)象緩存 private static List<Object> GLOBAL_CACHE = new ArrayList<Object>(8); // 線程本地變量 private static ThreadLocal<List<Object>> THREAD_CACHE = new ThreadLocal<List<Object>>(); // 嘗試加鎖 public synchronized boolean lock(Object obj){ if(this.contains(obj)){ return false; } List al = null; if((al=THREAD_CACHE.get())==null){ al = new ArrayList(2); THREAD_CACHE.set(al); } al.add(obj); GLOBAL_CACHE.add(obj); return true; } // 判斷是否存在key public boolean contains(Object obj){ List<Object> objs; return GLOBAL_CACHE.contains(obj)?true:(objs=THREAD_CACHE.get())==null?false:objs.contains(obj); } // 釋放key鎖,與上面的 lock 方法對(duì)應(yīng) public boolean release(Object obj){ if( this.contains(obj) ){ List<Object> objs = THREAD_CACHE.get(); if(null!=objs){ objs.remove(obj); GLOBAL_CACHE.remove(obj); } return true; } return false; } }
三、測(cè)試代碼
因?yàn)槭擎i,所以必須要使用多線程測(cè)試,這里我簡單使用 parallel stream +多輪循環(huán)去測(cè)試:
public class CacheObjectLockTest { private CacheObjectLock LOCK = new CacheObjectLock(); public void test1(){ IntStream.range(0,10000).parallel().forEach(i->{ if(i%3==0){ i-=2; } Boolean b = null; if((b=LOCK.lock(i))==false ){ return ; } Boolean c = null; try { // do something ... // TimeUnit.MILLISECONDS.sleep(1); } catch (Exception e) { throw new RuntimeException(e); }finally { c = LOCK.release(i); } if(b!=c){ System.out.println("b:"+b+" c:"+c+" => "+Thread.currentThread().getName()); } }); // LOCK.contains(9); } @Test public void test2(){ for(int i=0;i<10;i++){ this.test1(); } } }
測(cè)試結(jié)果
分析
顯而易見,這是沒有對(duì) release 加鎖導(dǎo)致的,其實(shí)呢,這樣說是不準(zhǔn)確的…
首先要明白 lock 上加的 synchronized 的同步鎖的范圍是對(duì)當(dāng)前實(shí)例的,而 release 是沒有加 synchronized ,所以 release 是無視 lock 上加的 synchronized
再仔細(xì)看看 GLOBAL_CACHE 是什么?ArrayList ,明白了吧 ArrayList 不是線程安全的,因?yàn)?synchronized 的范圍只是 lock 函數(shù)這一 函數(shù)內(nèi) ,從測(cè)試代碼可看到 LOCK.lock(i)
開始一直到 LOCK.release(i) 這中間是沒有加同步鎖的,所以到 LOCK.lock(i) 開始一直到 LOCK.release(i) 這中間是存在線程競(jìng)爭(zhēng)的,恰好又碰到 ArrayList 這一不安全因素自然會(huì)拋錯(cuò)的!
因?yàn)榇嬖诓话踩悾晕覀冇欣碛蓱岩?THREAD_CACHE 的泛型變量也是存在多線程異常的,因?yàn)樗@個(gè)泛型也是 ArrayList !
四、解決鎖問題
好了,明白了問題之所在,自然解決辦法也十分easy:
在 release 方法上添加 synchronized 聲明,這樣簡單粗暴
分別對(duì) objs.remove(obj); 以及 GLOBAL_CACHE.remove(obj); 加同步鎖,這樣顆粒度更細(xì)
因?yàn)?synchronized 是寫?yīng)氄嫉?,所以無需在 contains 中單獨(dú)加鎖
代碼 (這里僅有 release 變更)
public synchronized boolean release(Object obj){ if( this.contains(obj) ){ List<Object> objs = THREAD_CACHE.get(); if(null!=objs){ // synchronized (objs){ objs.remove(obj); // } // synchronized (GLOBAL_CACHE){ GLOBAL_CACHE.remove(obj); // } } return true; } return false; }
測(cè)試結(jié)果
分析??
測(cè)試了多輪都是成功的,沒有任何異常,難道就一定沒有異常了???
非也,非也~~~
為了讓問題體現(xiàn)的的更清晰,先修改下測(cè)試用例并把 contains 方法置為 public,然后測(cè)試用例:
public class CacheObjectLockTest { private CacheObjectLock2 LOCK = new CacheObjectLock2(); public void test1(){ IntStream.range(0,10000).parallel().forEach(i->{ // String it = "K"+i; if(i%3==0){ i-=2; } Boolean b = null; if((b=LOCK.lock(i))==false ){ return ; } Boolean c = null; try { // do something ... // TimeUnit.MILLISECONDS.sleep(1); } catch (Exception e) { throw new RuntimeException(e); }finally { c = LOCK.release(i); } if(b!=c){ System.out.println("b:"+b+" c:"+c+" => "+Thread.currentThread().getName()); } }); LOCK.contains(9); } @Test public void test2(){ for(int i=0;i<10;i++){ this.test1(); } } }
在這一行打上斷點(diǎn) LOCK.contains(9); 然后逐步進(jìn)入到 ThreadLocal 的 get() 方法中:
看到?jīng)],雖然key已經(jīng)被移除的,但是 ThreadLocal 里面關(guān)聯(lián)的是 key外層的 ArrayList , 因?yàn)殚_發(fā)機(jī)配置都較好,一旦導(dǎo)致 ThreadLocal 膨脹,則 OOM 是必然的事兒!
我們知道 ThreadLocal 的基本特性,它會(huì)根據(jù)線程分開存放各自線程的所 set 進(jìn)來的對(duì)象,若沒有調(diào)用其 remove 方法,變量會(huì)一直存在 ThreadLocal 這個(gè) map 中,
若上述的測(cè)試代碼放在線程池里面被管理,線程池會(huì)根據(jù)負(fù)載會(huì)增減線程,如果每一次執(zhí)行上述代碼用的線程都不是固定的 ThreadLocal 必然會(huì)導(dǎo)致 jvm OOM ??
這就像 java 里面的 文件讀寫,open 之后必須要 要有 close 操作。
五、 解決ThreadLocal問題
最后更改代碼如下:
public class CacheObjectLock3 { private static List<Object> GLOBAL_CACHE = new ArrayList<Object>(8); private static ThreadLocal<List<Object>> THREAD_CACHE = new ThreadLocal<List<Object>>(); public synchronized boolean lock(Object obj){ if(this.contains(obj)){ return false; } List al = null; if((al=THREAD_CACHE.get())==null){ al = new ArrayList(2); THREAD_CACHE.set(al); } al.add(obj); GLOBAL_CACHE.add(obj); return true; } public boolean contains(Object obj){ List<Object> objs; return GLOBAL_CACHE.contains(obj)?true:(objs=THREAD_CACHE.get())==null?false:objs.contains(obj); } public synchronized boolean release(Object obj){ if( this.contains(obj) ){ List<Object> objs = THREAD_CACHE.get(); if(null!=objs){ // synchronized (objs){ objs.remove(obj); if(objs.isEmpty()){ THREAD_CACHE.remove(); } // } // synchronized (GLOBAL_CACHE){ GLOBAL_CACHE.remove(obj); // } } return true; } return false; } }
測(cè)試結(jié)果
測(cè)試 ok 通過 ~
總結(jié)
到此這篇關(guān)于Java線程本地變量導(dǎo)致的緩存問題解決方法的文章就介紹到這了,更多相關(guān)Java線程本地變量緩存問題內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
淺析Java關(guān)鍵詞synchronized的使用
Synchronized是java虛擬機(jī)為線程安全而引入的。這篇文章主要為大家介紹一下Java關(guān)鍵詞synchronized的使用與原理,需要的可以參考一下2022-12-12Java8需要知道的4個(gè)函數(shù)式接口簡單教程
這篇文章主要介紹了Java?8中引入的函數(shù)式接口,包括Consumer、Supplier、Predicate和Function,以及它們的用法和特點(diǎn),文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-03-03springboot整合mybatis實(shí)現(xiàn)多表查詢的實(shí)戰(zhàn)記錄
SpringBoot對(duì)數(shù)據(jù)庫操作有多種方式,下面這篇文章主要給大家介紹了關(guān)于springboot整合mybatis實(shí)現(xiàn)多表查詢的相關(guān)資料,文中通過示例代碼以及圖文介紹的非常詳細(xì),需要的朋友可以參考下2021-08-08Java設(shè)計(jì)模式以虹貓藍(lán)兔的故事講解橋接模式
橋接是用于把抽象化與實(shí)現(xiàn)化解耦,使二者可以獨(dú)立變化。這種類型的設(shè)計(jì)模式屬于結(jié)構(gòu)型模式,它通過提供抽象化和實(shí)現(xiàn)化之間的橋接結(jié)構(gòu),來實(shí)現(xiàn)二者的解耦。這種模式涉及到一個(gè)作為橋接的接口,使得實(shí)體類的功能獨(dú)立于接口實(shí)現(xiàn)類。這兩種類型的類可被結(jié)構(gòu)化改變而互不影響2022-04-04Win11系統(tǒng)下載安裝java的詳細(xì)過程
這篇文章主要介紹了Win11如何下載安裝java,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-05-05一文帶你掌握J(rèn)ava8中Lambda表達(dá)式 函數(shù)式接口及方法構(gòu)造器數(shù)組的引用
Java 8 (又稱為 jdk 1.8) 是 Java 語言開發(fā)的一個(gè)主要版本。 Oracle 公司于 2014 年 3 月 18 日發(fā)布 Java 8 ,它支持函數(shù)式編程,新的 JavaScript 引擎,新的日期 API,新的Stream API 等2021-10-10spring boot 使用profile來分區(qū)配置的操作
這篇文章主要介紹了spring boot使用profile來分區(qū)配置的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07