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

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

如有转载请著名出处: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 修饰

好了,准备看代码 😂

先看第一版

  • 代码
java 复制代码
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,然后测试用例:

java 复制代码
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 操作。

最后更改

  • 代码
java 复制代码
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 的时候务必谨慎~

核心代码是仅是部分截取过来的,如存在问题烦请告知于我,在此感谢了 ♥

相关推荐
保持学习ing1 分钟前
SpringBoot 前后台交互 -- CRUD
java·spring boot·后端·ssm·项目实战·页面放行
啾啾Fun1 小时前
Java反射操作百倍性能优化
java·性能优化·反射·缓存思想
20岁30年经验的码农1 小时前
若依微服务Openfeign接口调用超时问题
java·微服务·架构
曲莫终1 小时前
SpEl表达式之强大的集合选择(Collection Selection)和集合投影(Collection Projection)
java·spring boot·spring
ajassi20002 小时前
开源 java android app 开发(十二)封库.aar
android·java·linux·开源
q567315232 小时前
Java使用Selenium反爬虫优化方案
java·开发语言·分布式·爬虫·selenium
kaikaile19952 小时前
解密Spring Boot:深入理解条件装配与条件注解
java·spring boot·spring
守护者1702 小时前
JAVA学习-练习试用Java实现“一个词频统计工具 :读取文本文件,统计并输出每个单词的频率”
java·学习
bing_1582 小时前
Spring Boot 中ConditionalOnClass、ConditionalOnMissingBean 注解详解
java·spring boot·后端
ergdfhgerty2 小时前
斐讯N1部署Armbian与CasaOS实现远程存储管理
java·docker