首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

同时使用线程本地变量以及对象缓存的问题

编程知识
2024年07月20日 21:36

同时使用线程本地变量以及对象缓存的问题

如有转载请著名出处:https://www.cnblogs.com/funnyzpc/p/18313879

前面

  前些时间看别人写的一段关于锁的(对象缓存+线程本地变量)的一段代码,这段代码大致描述了这么一个功能:
外部传入一个key,需要根据这个key去全局变量里面找是否存在,如有有则表示有人对这个key加锁了,往下就不执行具体业务代码,同时,同时哦 还要判断这个key是不是当前线程持有的,如果不是当前线程持有的也不能往下执行业务代码~
   然后哦 还要在业务代码执行完成后释放这个key锁,也就是要从 ThreadLocal 里面移除这个key。
  当然需求不仅于此,就是业务的特殊性需要 ThreadLocal 同时持有多个不同的key,这就表明 ThreadLocal 的泛型肯定是个List或Set。
  然后再说下代码,为了演示问题代码写的比较简略,以下我再一一说明可能存在的问题🎈

基本逻辑

功能大致包含两个函数:

  • lock : 主要是查找公共缓存还有线程本地变量是否包含传入的指定key,若无则尝试写入全局变量及 ThreadLocal 并返回true以示获取到锁
  • release : 业务逻辑处理完成后调用此,此函数内主要是做全局缓存以及 ThreadLocal 内的key的移除并返回状态(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 方法对应 
    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 +多轮循环去测试:

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();
        }
    }
}
  • 测试结果

  • 分析

    显而易见,这是没有对 release 加锁导致的,其实呢,这样说是不准确的...
    首先要明白 lock 上加的 synchronized 的同步锁的范围是对当前实例的,而 release 是没有加 synchronized ,所以 release 是无视 lock 上加的 synchronized
    再仔细看看 GLOBAL_CACHE 是什么?ArrayList ,明白了吧 ArrayList 不是线程安全的,因为 synchronized 的范围只是 lock 函数这一 函数内 ,从测试代码可看到 LOCK.lock(i)
    开始一直到 LOCK.release(i) 这中间是没有加同步锁的,所以到 LOCK.lock(i) 开始一直到 LOCK.release(i) 这中间是存在线程竞争的,恰好又碰到 ArrayList 这一不安全因素自然会抛错的!
    因为存在不安全类,所以我们有理由怀疑 THREAD_CACHE 的泛型变量也是存在多线程异常的,因为它这个泛型也是 ArrayList

再看第二版

好了,明白了问题之所在,自然解决办法也十分easy:

  1. release 方法上添加 synchronized 声明,这样简单粗暴
  2. 分别对 objs.remove(obj); 以及 GLOBAL_CACHE.remove(obj); 加同步锁,这样颗粒度更细

因为 synchronized 是写独占的,所以无需在 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;
    }
  • 测试结果

  • 分析

    😂
    测试了多轮都是成功的,没有任何异常,难道就一定没有异常了???
    非也,非也~~~
    为了让问题体现的的更清晰,先修改下测试用例并把 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); 然后逐步进入到 ThreadLocalget() 方法中:

看到没,虽然key已经被移除的,但是 ThreadLocal 里面关联的是 key外层的 ArrayList , 因为开发机配置都较好,一旦导致 ThreadLocal 膨胀,则 OOM 是必然的事儿!
我们知道 ThreadLocal 的基本特性,它会根据线程分开存放各自线程的所 set 进来的对象,若没有调用其 remove 方法,变量会一直存在 ThreadLocal 这个 map 中,
若上述的测试代码放在线程池里面被管理,线程池会根据负载会增减线程,如果每一次执行上述代码用的线程都不是固定的 ThreadLocal 必然会导致 jvm OOM 😂
这就像 java 里面的 文件读写,open 之后必须要 要有 close 操作。

最后更改

  • 代码
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;
    }

}

  • 测试结果
    (截图略)
    测试 ok 通过 ~

最后

以上代码未必是完美的,但至少看到了问题所在,尤其使用 ThreadLocal 的时候务必谨慎~
核心代码是仅是部分截取过来的,如存在问题烦请告知于我,在此感谢了 ♥

From:https://www.cnblogs.com/funnyzpc/p/18313879
本文地址: http://shuzixingkong.net/article/232
0评论
提交 加载更多评论
其他文章 音频文件降噪及python示例
操作系统 :Windows 10_x64 Python版本:3.9.2 noisereduce版本:3.0.2 从事音频相关工作,大概率会碰到降噪问题,今天整理下之前学习音频文件降噪的笔记,并提供Audacity和python示例。 我将从以下几个方面展开: noisereduce库介绍 使用Aud
音频文件降噪及python示例 音频文件降噪及python示例 音频文件降噪及python示例
LLM并行训练6-激活优化
LLM训练activation优化相关技术, 包括激活重计算/序列并行/zero-R/zero-offload等
LLM并行训练6-激活优化 LLM并行训练6-激活优化 LLM并行训练6-激活优化
玄机-第二章日志分析-mysql应急响应
玄机-第二章日志分析-mysql应急响应 mysql应急响应 ssh账号 root 密码 xjmysql ssh root@env.xj.edisec.net -p 端口号 1.黑客第一次写入的shell flag{关键字符串} 2.黑客反弹shell的ip flag{ip} 3.黑客提权文件
玄机-第二章日志分析-mysql应急响应 玄机-第二章日志分析-mysql应急响应 玄机-第二章日志分析-mysql应急响应
Known框架实战演练——进销存数据结构
系统主要包含商品信息、商业伙伴(客户、供应商)信息、业务单表头信息、业务单表体信息、对账单表头信息、对账单表体信息。 1. 商品信息(JxGoods) 该表用于存储公司商品信息。 名称 代码 类型 长度 必填 商品信息 JxGoods 商品编码 Code Text 50 Y 商品名称 Name Te
keepalived
四、Keepalived 【1】、keepalived运行原理 Keepalived检测每个服务器接节点状态 服务器节点异常或出现工作故障,keepalived将故障节点从集群系统中剔除 故障节点恢复后,Keepalived再将其加入到集群系统中 所有工作自动完成,无需人工干预 keepalived
keepalived
如何在.NET Framework及.NET8以前项目中使用C#12新特性
前两天发了一篇关于模式匹配的文章,链接地址,有小伙伴提到使用.NET6没法体验 C#新特性的疑问, 其实呢只要本地的SDK源代码编译器能支持到的情况下(直接下载VS2022或者VS的最新preview版本) 只需要做很小的改动就可以支持的. 目前仍然还有一些小伙伴因为历史原因可能还在写.NET Fr
如何在.NET Framework及.NET8以前项目中使用C#12新特性
GIS前沿技术
无论是初步接触到GIS的学生,还是对GIS已经有一定的了解的从业者,肯定都非常关心两个问题:GIS有没有发展前景,GIS有哪些应用价值? 关于这两个问题,笔者的答案是GIS作为一门融合了空间数据采集、存储、处理、分析和可视化的学科,涉及到多个交叉领域和技术,因此非常容易与时下流行的前沿科技相结合,因
GIS前沿技术 GIS前沿技术 GIS前沿技术
Standard Template Libary or C++ Standard Library
C++提供一套标准的library称为C++ standard library完全以template完成,所以又被称为Standard Template Library。这套library专门有于实现常用的据结构(例如arry、list......)以及常用的算法(例如push,pop,insert