Java線程本地變量導(dǎo)致的緩存問題解決方法
一、前言
前些時間看別人寫的一段關(guān)于鎖的(對象緩存+線程本地變量)的一段代碼,這段代碼大致描述了這么一個功能:
外部傳入一個key,需要根據(jù)這個key去全局變量里面找是否存在,如有有則表示有人對這個key加鎖了,往下就不執(zhí)行具體業(yè)務(wù)代碼,同時,同時哦 還要判斷這個key是不是當前線程持有的,如果不是當前線程持有的也不能往下執(zhí)行業(yè)務(wù)代碼~
然后哦 還要在業(yè)務(wù)代碼執(zhí)行完成后釋放這個key鎖,也就是要從 ThreadLocal 里面移除這個key。
當然需求不僅于此,就是業(yè)務(wù)的特殊性需要 ThreadLocal 同時持有多個不同的key,這就表明 ThreadLocal 的泛型肯定是個List或Set。
然后再說下代碼,為了演示問題代碼寫的比較簡略,以下我再一一說明可能存在的問題??
二、基本邏輯
功能大致包含兩個函數(shù):
lock : 主要是查找公共緩存還有線程本地變量是否包含傳入的指定key,若無則嘗試寫入全局變量及 ThreadLocal 并返回true以示獲取到鎖
release : 業(yè)務(wù)邏輯處理完成后調(diào)用此,此函數(shù)內(nèi)主要是做全局緩存以及 ThreadLocal 內(nèi)的key的移除并返回狀態(tài)(true/false)
contains : 公共方法,供以上兩個方法使用,邏輯:判斷全局變量或 ThreadLocal 里面有否有指定的key,此方法用 private 修飾
好了,準備看代碼 ??
代碼如下:
public class CacheObjectLock { // 全局對象緩存 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 方法對應(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; } }
三、測試代碼
因為是鎖,所以必須要使用多線程測試,這里我簡單使用 parallel stream +多輪循環(huán)去測試:
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(); } } }
測試結(jié)果
分析
顯而易見,這是沒有對 release 加鎖導(dǎo)致的,其實呢,這樣說是不準確的…
首先要明白 lock 上加的 synchronized 的同步鎖的范圍是對當前實例的,而 release 是沒有加 synchronized ,所以 release 是無視 lock 上加的 synchronized
再仔細看看 GLOBAL_CACHE 是什么?ArrayList ,明白了吧 ArrayList 不是線程安全的,因為 synchronized 的范圍只是 lock 函數(shù)這一 函數(shù)內(nèi) ,從測試代碼可看到 LOCK.lock(i)
開始一直到 LOCK.release(i) 這中間是沒有加同步鎖的,所以到 LOCK.lock(i) 開始一直到 LOCK.release(i) 這中間是存在線程競爭的,恰好又碰到 ArrayList 這一不安全因素自然會拋錯的!
因為存在不安全類,所以我們有理由懷疑 THREAD_CACHE 的泛型變量也是存在多線程異常的,因為它這個泛型也是 ArrayList !
四、解決鎖問題
好了,明白了問題之所在,自然解決辦法也十分easy:
在 release 方法上添加 synchronized 聲明,這樣簡單粗暴
分別對 objs.remove(obj); 以及 GLOBAL_CACHE.remove(obj); 加同步鎖,這樣顆粒度更細
因為 synchronized 是寫?yīng)氄嫉?,所以無需在 contains 中單獨加鎖
代碼 (這里僅有 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; }
測試結(jié)果
分析??
測試了多輪都是成功的,沒有任何異常,難道就一定沒有異常了???
非也,非也~~~
為了讓問題體現(xiàn)的的更清晰,先修改下測試用例并把 contains 方法置為 public,然后測試用例:
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(); } } }
在這一行打上斷點 LOCK.contains(9); 然后逐步進入到 ThreadLocal 的 get() 方法中:
看到?jīng)],雖然key已經(jīng)被移除的,但是 ThreadLocal 里面關(guān)聯(lián)的是 key外層的 ArrayList , 因為開發(fā)機配置都較好,一旦導(dǎo)致 ThreadLocal 膨脹,則 OOM 是必然的事兒!
我們知道 ThreadLocal 的基本特性,它會根據(jù)線程分開存放各自線程的所 set 進來的對象,若沒有調(diào)用其 remove 方法,變量會一直存在 ThreadLocal 這個 map 中,
若上述的測試代碼放在線程池里面被管理,線程池會根據(jù)負載會增減線程,如果每一次執(zhí)行上述代碼用的線程都不是固定的 ThreadLocal 必然會導(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; } }
測試結(jié)果
測試 ok 通過 ~
總結(jié)
到此這篇關(guān)于Java線程本地變量導(dǎo)致的緩存問題解決方法的文章就介紹到這了,更多相關(guān)Java線程本地變量緩存問題內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
淺析Java關(guān)鍵詞synchronized的使用
Synchronized是java虛擬機為線程安全而引入的。這篇文章主要為大家介紹一下Java關(guān)鍵詞synchronized的使用與原理,需要的可以參考一下2022-12-12springboot整合mybatis實現(xiàn)多表查詢的實戰(zhàn)記錄
SpringBoot對數(shù)據(jù)庫操作有多種方式,下面這篇文章主要給大家介紹了關(guān)于springboot整合mybatis實現(xiàn)多表查詢的相關(guān)資料,文中通過示例代碼以及圖文介紹的非常詳細,需要的朋友可以參考下2021-08-08一文帶你掌握Java8中Lambda表達式 函數(shù)式接口及方法構(gòu)造器數(shù)組的引用
Java 8 (又稱為 jdk 1.8) 是 Java 語言開發(fā)的一個主要版本。 Oracle 公司于 2014 年 3 月 18 日發(fā)布 Java 8 ,它支持函數(shù)式編程,新的 JavaScript 引擎,新的日期 API,新的Stream API 等2021-10-10spring boot 使用profile來分區(qū)配置的操作
這篇文章主要介紹了spring boot使用profile來分區(qū)配置的操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07