欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Java線程本地變量導(dǎo)致的緩存問題解決方法

 更新時(shí)間:2024年08月30日 10:53:28   作者:從零開始的-CodeNinja之路  
使用緩存可以緩解大流量壓力,顯著提高程序的性能,我們?cè)谑褂镁彺嫦到y(tǒng)時(shí),尤其是大并發(fā)情況下,經(jīng)常會(huì)遇到一些疑難雜癥,這篇文章主要給大家介紹了關(guān)于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的使用

    淺析Java關(guān)鍵詞synchronized的使用

    Synchronized是java虛擬機(jī)為線程安全而引入的。這篇文章主要為大家介紹一下Java關(guān)鍵詞synchronized的使用與原理,需要的可以參考一下
    2022-12-12
  • Java8需要知道的4個(gè)函數(shù)式接口簡單教程

    Java8需要知道的4個(gè)函數(shù)式接口簡單教程

    這篇文章主要介紹了Java?8中引入的函數(shù)式接口,包括Consumer、Supplier、Predicate和Function,以及它們的用法和特點(diǎn),文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2025-03-03
  • Spring MVC處理響應(yīng)的案例詳解

    Spring MVC處理響應(yīng)的案例詳解

    當(dāng)服務(wù)器向客戶端響應(yīng)數(shù)據(jù)時(shí),SpringMVC框架會(huì)使用“轉(zhuǎn)換器”(Converter)將方法的返回值進(jìn)行轉(zhuǎn)換,SpringMVC框架還會(huì)自動(dòng)使用不同的轉(zhuǎn)換器,因此這篇文章就給大家詳細(xì)介紹一下Spring MVC如何處理響應(yīng)并附上案例,需要的朋友可以參考下
    2023-06-06
  • springboot整合mybatis實(shí)現(xiàn)多表查詢的實(shí)戰(zhàn)記錄

    springboot整合mybatis實(shí)現(xiàn)多表查詢的實(shí)戰(zhàn)記錄

    SpringBoot對(duì)數(shù)據(jù)庫操作有多種方式,下面這篇文章主要給大家介紹了關(guān)于springboot整合mybatis實(shí)現(xiàn)多表查詢的相關(guān)資料,文中通過示例代碼以及圖文介紹的非常詳細(xì),需要的朋友可以參考下
    2021-08-08
  • Java設(shè)計(jì)模式以虹貓藍(lán)兔的故事講解橋接模式

    Java設(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-04
  • Win11系統(tǒng)下載安裝java的詳細(xì)過程

    Win11系統(tǒng)下載安裝java的詳細(xì)過程

    這篇文章主要介紹了Win11如何下載安裝java,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2023-05-05
  • 一文帶你掌握J(rèn)ava8中Lambda表達(dá)式 函數(shù)式接口及方法構(gòu)造器數(shù)組的引用

    一文帶你掌握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-10
  • Spring中的@Aspect注解使用詳解

    Spring中的@Aspect注解使用詳解

    這篇文章主要介紹了Spring中的@Aspect注解使用詳解,利用AOP可以對(duì)業(yè)務(wù)邏輯的各個(gè)部分進(jìn)行隔離,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低,提高程序的可重用性,同時(shí)提高了開發(fā)的效率,需要的朋友可以參考下
    2024-01-01
  • spring boot 使用profile來分區(qū)配置的操作

    spring boot 使用profile來分區(qū)配置的操作

    這篇文章主要介紹了spring boot使用profile來分區(qū)配置的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-07-07
  • Mybatis中SQL的執(zhí)行過程詳解

    Mybatis中SQL的執(zhí)行過程詳解

    MyBatis框架通過映射文件或注解將Java代碼中的方法與數(shù)據(jù)庫操作進(jìn)行映射,執(zhí)行過程包括SQL解析、參數(shù)綁定、SQL預(yù)編譯、執(zhí)行、結(jié)果映射、事務(wù)處理、緩存處理和日志記錄
    2024-12-12

最新評(píng)論