ThreadLocal及其应用浅析
参考视频:https://www.bilibili.com/video/BV1fA411b7SX?from=search&seid=4082488691777652024
参考博客:https://www.cnblogs.com/micrari/p/6790229.html#3835852
TheadLocal<T> tl = new ThreadLocal<>();
dosomething();
tl.set(x);
ThreadLocal将变量副本与线程进行绑定,实现了变量的线程隔离。这也意味着,tl.set(x)
完成后,即便没有显示地传参,也可以在该线程执行的过程中,在想要的位置取值tl.get()
。在spring的@Transactional,mybatis的分页插件PageHelper,链日志中都有应用。
ThreadLocal原理
ThreadLcoal的源码注释较为详细,源码量也十分友好,能比较方便地阅读,这里将对一些关键代码进行梳理。当ThreadLocal对象进行set操作设置本地变量时会从当前线程中取到一个ThreadLocalMap。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); //click
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; //每个线程都有一个类型为ThreadLocal.ThreadLocalMap的字段threadLocals
}
ThreadLocalMap是ThreadLocal中的一个内部类,它里面还有一个Entry的内部类,Entry封装了一个键值对,以ThreadLocal为键,以需要装载的本地变量为value。
因为一个线程里可以有多个ThreadLocal,所以ThreadLocalMap设计了Entry数组private Entry[] table;
。
ThreadLocal的内存泄露问题
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
从源码不难看出Entry(ThreadLocal<?> k, Object v)
中的key是对ThreadLocal的一个弱引用。结合原理图,①当tl=null
时,内存中内被new出来的ThreadLocal由于只被弱引用,该块内存将被回收。加入该弱引用改为强引用,即便tl=null
,图中的ThreadLocal也不会被回收,弱引用解决了这一块的内存泄露问题。②但是ThreadLocalMap中保存的本地变量10Mvalue仍然被强引用,如果线程长时间不进行get和set(当ThreadLocal进行get和set的时候将会清理这些key=null的值),那么这块内存将不会被回收,这就造成了内存泄露的问题。下面对此进行验证:
可以发现,utl=null
然后进行垃圾回收,Entry中的value依旧存在。所以在使用threadLocal时要养成显示remove的习惯,它还能解决线程复用带来的问题。
@Slf4j
@RestController
public class ThreadLocalTestController {
private static ThreadLocal<Integer> i = new ThreadLocal<>();
@GetMapping("testsingleton1")
@ResponseBody
public int test1() {
if (i.get() == null) {
i.set(0);
}
i.set(i.get().intValue() + 1);
log.info("{} -> {}", Thread.currentThread().getName(), i.get());
int res = i.get().intValue();
// i.remove(); //避免线程复用带来并发问题
return res;
}
}
controller中有一个ThreadLocal,每当有请求就会对ThreadLocal中的i值加1。测试时发了30个并发请求,对于这30个请求,期望打印的日志中i值都为1,但是实际上i值被累加
原因在于spring开启了线程池,线程被复用了,i.get().intValue()
拿到的是线程直线被使用的值。为了解决这个问题需要手动对ThreadLocal进行remove,即把上面代码中的注释打开。