一、定义
ThreadLocal是线程私有变量,用于保存每个线程的私有数据。
那么什么情况下需要进行线程隔离
二、源码分析
public class ThreadLocalTest01 {
ThreadLocal<Integer> t = new ThreadLocal<>();
public void test() {
t.set(1);
Integer integer = t.get();
}
}
set
在调用set方法时,会先获取当前线程,然后获取当前线程的ThreadLocalMap,判断map是否存在,若不存在,则把创建一个ThreadLocalMap,若存在,则以当前的ThreadLocal作为key,传入的参数作为value存入map中。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
其中ThreadLocalMap是ThreadLocal的内部类,它有一个静态内部类Entry,继承自WeakReference
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
get()
获取当前线程,获取当前线程的ThreadLocalMap,然后用当前的ThreadLocal作为key 去map中查找,如果存在对应的Entry,那么就返回Entry中的value,否则就会执行初始化并返回默认值,其实就是null
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}
protected T initialValue() {
return null;
}
三、注意的问题
3.1 为什么使用弱引用
主要两个原因
1 . 没有手动删除这个 Entry
2 . CurrentThread 当前线程依然运行
原因是使用ThreadLocal有可能会导致内存泄漏,使用弱引用能解决一部分内存泄漏
第一点很好理解,只要在使用完下 ThreadLocal ,调用其 remove 方法删除对应的 Entry ,就能避免内存泄漏。
第二点稍微复杂一点,由于ThreadLocalMap 是 Thread 的一个属性,被当前线程所引用,所以ThreadLocalMap的生命周期跟 Thread 一样长。如果threadlocal变量被回收,那么当前线程的threadlocal 变量副本指向的就是key=null, 也即entry(null,value),那这个entry对应的value永远无法访问到。实际私用ThreadLocal场景都是采用线程池,而线程池中的线程都是复用的,这样就可能导致非常多的entry(null,value)出现,从而导致内存泄露。
综上, ThreadLocal 内存泄漏的根源是:
由于ThreadLocalMap 的生命周期跟 Thread 一样长,对于重复利用的线程来说,如果没有手动删除(remove()方法)对应 key 就会导致entry(null,value)的对象越来越多,从而导致内存泄漏.
3.1.1 、key 如果是强引用
那么为什么ThreadLocalMap的key要设计成弱引用呢?其实很简单,如果key设计成强引用且没有手动remove(),那么key会和value一样伴随线程的整个生命周期。
1、假设在业务代码中使用完ThreadLocal, ThreadLocal ref被回收了,但是因为threadLocalMap的Entry强引用了threadLocal(key就是threadLocal), 造成ThreadLocal无法被回收。在没有手动删除Entry以及CurrentThread(当前线程)依然运行的前提下, 始终有强引用链CurrentThread Ref → CurrentThread →Map(ThreadLocalMap)-> entry, Entry就不会被回收( Entry中包括了ThreadLocal实例和value), 导致Entry内存泄漏也就是说: ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的。请结合图1看。
3.1.2 那么为什么 key 要用弱引用
事实上,在 ThreadLocalMap 中的set/getEntry 方法中,会对 key 为 null(也即是 ThreadLocal 为 null )进行判断,如果为 null 的话,那么会把 value 置为 null 的.这就意味着使用threadLocal , CurrentThread 依然运行的前提下.就算忘记调用 remove 方法,弱引用比强引用可以多一层保障:弱引用的 ThreadLocal 会被回收.对应value在下一次 ThreadLocaI 调用 get()/set()/remove() 中的任一方法的时候会被清除,从而避免内存泄漏.
3.2 发生Hash冲突
ThreadLocalMap的结构非常简单只用一个数组存储,并没有链表结构,当出现Hash冲突时采用线性查找的方式,所谓线性查找,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。如果产生多次hash冲突,处理起来就没有HashMap的效率高,为了避免哈希冲突,使用尽量少的threadlocal变量。
四、实际项目中的使用
每个用户调用我们的项目时,都会创建一个新的HTTP请求线程,这一次请求中会调用类A的方法testA()和B.class中的testB()方法,且都需要用到当前用户的某些信息,如用户名、Cookie、账号密码等信息。
调用两次接口传入不同的值,可以看到这一次请求中获得的值是一致的,而不同的调用请求获得的值是不同的。