同时使用线程本地变量以及对象缓存的问题
如有转载请著名出处: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:
- 在
release
方法上添加synchronized
声明,这样简单粗暴 - 分别对
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);
然后逐步进入到 ThreadLocal
的 get()
方法中:
看到没,虽然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
的时候务必谨慎~
核心代码是仅是部分截取过来的,如存在问题烦请告知于我,在此感谢了 ♥