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

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

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

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

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

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

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

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

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

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

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

    Java設(shè)計模式以虹貓藍兔的故事講解橋接模式

    橋接是用于把抽象化與實現(xiàn)化解耦,使二者可以獨立變化。這種類型的設(shè)計模式屬于結(jié)構(gòu)型模式,它通過提供抽象化和實現(xiàn)化之間的橋接結(jié)構(gòu),來實現(xiàn)二者的解耦。這種模式涉及到一個作為橋接的接口,使得實體類的功能獨立于接口實現(xiàn)類。這兩種類型的類可被結(jié)構(gòu)化改變而互不影響
    2022-04-04
  • Win11系統(tǒng)下載安裝java的詳細過程

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

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

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

    Spring中的@Aspect注解使用詳解

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

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

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

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

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

最新評論