广告位联系
返回顶部
分享到

Java线程本地变量导致的缓存问题解决方法

java 来源:互联网 作者:佚名 发布时间:2024-09-01 08:49:11 人浏览
摘要

前些时间看别人写的一段关于锁的(对象缓存+线程本地变量)的一段代码,这段代码大致描述了这么一个功能: 外部传入一个key,需要根据这个key去全局变量里面找是否存在,如有有则表示有

前些时间看别人写的一段关于锁的(对象缓存+线程本地变量)的一段代码,这段代码大致描述了这么一个功能:

外部传入一个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 修饰
好了,准备看代码 ????

代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

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 +多轮循环去测试:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

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:

在 release 方法上添加 synchronized 声明,这样简单粗暴

分别对 objs.remove(obj); 以及 GLOBAL_CACHE.remove(obj); 加同步锁,这样颗粒度更细

因为 synchronized 是写独占的,所以无需在 contains 中单独加锁

代码 (这里仅有 release 变更)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

    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,然后测试用例:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

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() 方法中:

看到没,虽然key已经被移除的,但是 ThreadLocal 里面关联的是 key外层的 ArrayList , 因为开发机配置都较好,一旦导致 ThreadLocal 膨胀,则 OOM 是必然的事儿!

我们知道 ThreadLocal 的基本特性,它会根据线程分开存放各自线程的所 set 进来的对象,若没有调用其 remove 方法,变量会一直存在 ThreadLocal 这个 map 中,

若上述的测试代码放在线程池里面被管理,线程池会根据负载会增减线程,如果每一次执行上述代码用的线程都不是固定的 ThreadLocal 必然会导致 jvm OOM ????

这就像 java 里面的 文件读写,open 之后必须要 要有 close 操作。

五、 解决ThreadLocal问题

最后更改代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

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 通过 ~


版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。
原文链接 :
相关文章
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计