ThreadLocal简介
多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。
ThreadLocal提供线程本地变量,每个线程拥有本地变量的副本,各个线程之间的变量互不干扰。ThreadLocal实现在多线程环境下去保证变量的安全。
ThreadLocal简单使用
//初始化ThreadLocal
ThreadLocal<String> threadLocal = new ThreadLocal<>();
//ThreadLocal设置value值
threadLocal.set("value");
//ThreadLocal获取value值
threadLocal.get();
//ThreadLocal移除操作
threadLocal.remove();
ThreadLocal源码解析
set设置value值
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取ThreadLocalMap
ThreadLocalMap map = getMap(t);
//ThreadLocalMap存在则设置值
//ThreadLocalMap不存在创建ThreadLocalMap
if (map != null)
//key为ThreadLocal变量
//value为传入value变量
map.set(this, value);
else
createMap(t, value);
}
看下getMap源码,直接取Thrad.threadLocals变量
可知,ThreadLocalMap存储在Thread类中,所以每个线程独有一份ThreadLocalMap。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//Thread类中变量
ThreadLocal.ThreadLocalMap threadLocals = null;
继续上createMap源码,直接调用ThreadLocalMap的构造函数
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//构造函数
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//初始化大小为16的Entry数组
table = new Entry[INITIAL_CAPACITY];
//根据key的hash计算数组下标
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//新建Entry对象,赋值给对应数组下标的位置
table[i] = new Entry(firstKey, firstValue);
//数组长度为1
size = 1;
//设置阀值 threshold = len * 2 / 3
setThreshold(INITIAL_CAPACITY);
}
针对计算下标的算法,HashMap和ConcurrentHashMap计算数组下标都是相似算法,说明下为何如此计算:
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
提取下为 hash & (size - 1),以size为默认16为例,其本质是为了hash值对应的下标均匀的分布在数组0-15位置。
假如不减1,key为随机数:
…0001 0000
&0101 1101
最终&运算结果只与16的二进制1的那位有关,最终结果只会有2个结果,0或16
size减1,key为随机数:
…0000 1111
&0101 1101
则最终结果与key的二进制的为1的位置有关,结果会在0-15之间
总共下set操作步骤:
1、取当前线程的ThreadLocalMap
2、ThreadLocalMap 已初始化则设置value值
3、未初始化则初始化
get获取value值
public T get() {
Thread t = Thread.currentThread();
//获取ThreadLocalMap
ThreadLocalMap map = getMap(t);
//ThreadLocalMap 存在,根据当前ThreadLocal实例获取value值
//不存在,调用setInitialValue(value默认null,初始化ThreadLocalMap)
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
setInitialValue源码
private T setInitialValue() {
//默认值null
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//ThreadLocalMap 不存在初始化
//存在设置value
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
remove移除
public void remove() {
//获取ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
//调用ThreadLocalMap.remove
if (m != null)
m.remove(this);
}
ThreadLocalMap.remove源码:
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
//根据hash计算数组下标
int i = key.threadLocalHashCode & (len-1);
//从计算的下标位置往后循环
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
//key相同,清除值
if (e.get() == key) {
e.clear();
//重新hash值
expungeStaleEntry(i);
return;
}
}
}
ThreadLocal内存泄露
首先我们看下ThreadLocal的内存引用情况
Thead类存放ThreadLocalMap :ThreadLocal.ThreadLocalMap threadLocals = null;
继续看ThreadLocalMap 类,存在的是Entry数组,Entry是个K V类型的内部类,K为ThreadLocal的弱引用,V为传入的value值。
static class ThreadLocalMap {
private Entry[] table;
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
上图为引用关系,实线表示强引用,虚线表示弱引用。
先说明下对象的引用级别:
强引用:对象具有强引用,垃圾回收器绝不会回收它,除非对象到 GC Roots 没有任何引用链相连。当内存空间不足,Java虚拟机会抛出OOM也不会回收强引用对象。
软引用:如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。
弱引用:在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。会存活至下一次GC。
虚引用:表示和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。
为什么会内存泄露:
假设在业务代码中使用完ThreadLocal, ThreadLocal引用被回收了
由于threadLocalMap只持有ThreadLocal的弱引用, 没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收, 此时Entry中的key = null
在没有手动删除Entry以及CurrentThread依然运行的前提下, 也存在始终有强引用链CurrentThread 引用 → CurrentThread →ThreadLocalMap-> entry,value 就不会被回收, 而这块value永远不会被访问到了(因为key=null), 导致value内存泄漏
解决办法:使用完ThreadLocal后需要调用remove方法,删除对应的 Entry ,就能避免内存泄漏。