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。
然后再說下代碼,為了演示問題代碼寫的比較簡(jiǎn)略,以下我再一一說明可能存在的問題??
二、基本邏輯
功能大致包含兩個(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è)試,這里我簡(jiǎn)單使用 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 聲明,這樣簡(jiǎn)單粗暴
分別對(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)文章
詳解springboot啟動(dòng)時(shí)是如何加載配置文件application.yml文件
這篇文章主要介紹了詳解springboot啟動(dòng)時(shí)是如何加載配置文件application.yml文件,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06
使用Java實(shí)現(xiàn)一個(gè)智能的圖片壓縮工具
在現(xiàn)代Web應(yīng)用中,圖片處理是一個(gè)常見且重要的需求,無論是用戶頭像、商品圖片還是訪客照片,都需要進(jìn)行適當(dāng)?shù)奶幚硪源_保系統(tǒng)性能和用戶體驗(yàn),本文將詳細(xì)介紹如何使用Java實(shí)現(xiàn)一個(gè)智能的圖片壓縮工具,需要的朋友可以參考下2025-08-08
java: 程序包c(diǎn)om.fasterxml.jackson.annotation不存在的解決辦法
當(dāng)我們?cè)趯?dǎo)入程序之后,系統(tǒng)給出錯(cuò)誤提示:java: 程序包c(diǎn)om.fasterxml.jackson.annotation不存在,本文主要介紹了Java程序包不存在的三種解決方法,需要的朋友可以參考下2024-02-02
java中l(wèi)ong和Long有什么區(qū)別詳解
這篇文章主要介紹了Java中l(wèi)ong和Long是基本數(shù)據(jù)類型和包裝數(shù)據(jù)類型的區(qū)別,包括默認(rèn)值、內(nèi)存占用、使用場(chǎng)景、方法支持以及裝箱和拆箱,包裝數(shù)據(jù)類型如Integer提供了許多有用的方法,需要的朋友可以參考下2025-02-02
Java?超詳細(xì)講解設(shè)計(jì)模式之中的建造者模式
建造者模式,是一種對(duì)象構(gòu)建模式 它可以將復(fù)雜對(duì)象的建造過程抽象出來,使這個(gè)抽象過程的不同實(shí)現(xiàn)方法可以構(gòu)造出不同表現(xiàn)的對(duì)象。本文將通過示例講解建造者模式,需要的可以參考一下2022-03-03
使用idea創(chuàng)建web框架和配置struts的方法詳解
這篇文章主要介紹了使用idea創(chuàng)建web框架和配置struts的方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09
Java詳解多線程協(xié)作作業(yè)之信號(hào)同步
信號(hào)量同步是指在不同線程之間,通過傳遞同步信號(hào)量來協(xié)調(diào)線程執(zhí)行的先后次序。CountDownLatch是基于時(shí)間維度的Semaphore則是基于信號(hào)維度的2022-05-05
全解史上最快的JOSN解析庫(kù)alibaba Fastjson
這篇文章主要介紹了史上最快的JOSN解析庫(kù)alibaba Fastjson,對(duì)FastJson感興趣的同學(xué),一定要看一下2021-04-04
使用java代碼實(shí)現(xiàn)一個(gè)月內(nèi)不再提醒,通用到期的問題
這篇文章主要介紹了使用java代碼實(shí)現(xiàn)一個(gè)月內(nèi)不再提醒,通用到期的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-01-01

