ThreadLocal简介
ThreadLocal线程本地变量的副本,对一个线程内的变量的修改不影响其它线程的变量。即在多线程环境下,可以保证各个线程之间的变量互相隔离、相互独立。
①ThreadLocal实例通常在类中被定义为private static
②ThreadLocal在线程的生命周期内起作用
③空间换时间的设计思想
ThreadLocal用法
首先了解下ThreadLocal的基本用法
private static ThreadLocal<Map<String, String>> threadLocal = new MyThreadLocal();
private static class MyThreadLocal extends ThreadLocal<Map<String, String>>{
@Override
protected Map<String, String> initialValue() {
Map map = new HashMap<String, String>();
map.put("andy","18");
return map;
}
}
public static void main(String[] args) {
Map<String, String> result = threadLocal.get();
System.out.println(result.get("andy"));
result.put("lvy", "28");
threadLocal.set(result);
System.out.println(result.get("andy"));
System.out.println(result.get("lvy"));
}
ThreadLocal源码分析
接下来分析各个方法的源码
// 将此线程局部变量的当前线程副本中的值设置为指定值
public void set(T value) {
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocalMap,有直接设置value、没有新建
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
// 创建ThreadLocalMap实例,并与当前线程绑定
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 返回此线程局部变量的当前线程副本中的值
setInitialValue()
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 从当前线程的ThreadLocalMap中查找Entry,如果不必为null返回value,否则设置初值并返回
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
// 移除此线程局部变量当前线程的值
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
// 从当前线程的ThreadLocalMap中删除
m.remove(this);
}
private T setInitialValue() {
// initialValue返回此线程局部变量的当前线程的初始值
T value = initialValue(); //未覆盖就是null
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
Thread与ThreadLocal的关系
Thread中有两个变量,类型都为ThreadLocal的静态内部类ThreadLocalMap,虽然被命名为Map,但和Map接口没任何关系,ThreadLocalMap底层是一个的散列表(可扩容的数组),并采用开放地址法来解决hash冲突。(解决hash冲突的方式,可以阅读数据结构中的一篇文章:https://blog.csdn.net/jiangtianjiao/article/details/88767614)
ThreadLocal.ThreadLocalMap threadLocals = null; // 在ThreadLocal的createMap方法中被赋值
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; // 在Thread的init方法中被赋值
ThreadLocalMap.Entry定义
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocal内存泄露分析
首先引入一张示意图,能帮助我们直观的理解threadLocal,threadLocalMap,entry之间的关系
上图实线代表强引用,虚线表示弱引用
如果ThreadLocal Ref断开,ThreadLocal就没有一条链路可达,GC必然会被回收,因此key为null,这样一来,通过key永远访问不了value,但是存在了这样一条引用链:threadRef->currentThread->threadLocalMap->entry->valueRef->valueMemory,导致value既不能被回收又不能被访问,一致占用内存,除非线程死亡。
而在实际开发过程中我们经常使用线程池来维护线程,如当我们通过Executors.newFixedThreadPool()时创建线程的时候,为了复用线程是不会结束的,所以极大可能导致内存泄漏。既然存在内存泄漏的问题,源码中必定存在相应的措施。
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); // 求数组下标
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) { // 如果能找到相同的ThreadLocal,则覆盖entry的value
e.value = value;
return;
}
if (k == null) { // 这里k=null,e不为空,表示ThreadLocal被回收了
replaceStaleEntry(key, value, i); // 处理脏entry
return;
}
}
tab[i] = new Entry(key, value); // 刚好下标i的entry为空,新建entry,直接插入
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
// 每个ThreadLocal对象都有一个hash值threadLocalHashCode,每初始化一个ThreadLocal对象
// hash值就增加一个固定的大小0x61c88647。
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i); // expunge删去
}
} while ( (n >>>= 1) != 0);
return removed;
}
cleanSomeSlots有两个参数:i表示set方法中刚新建插入的entry,n用于控制轮询次数,此处n是已经插入的size个数。如果从新插入entry开始,往后遍历过程中出现脏entry,则n更新为hash数组长度,然后进行expungeStaleEntry清理。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 清除当前脏entry
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
// 2.往后环形继续查找,直到遇到table[i]==null时结束
// 为null时不会成为要清理的对象
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//3. 如果在向后搜索过程中再次遇到脏entry,同样将其清理掉
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
//处理rehash的情况
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
//向前找到第一个脏entry
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == key) {
// 如果在向后环形查找过程中发现key相同的entry就覆盖并且和脏entry进行交换
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
// 如果在查找过程中还未发现脏entry,那么就以当前位置作为cleanSomeSlots的起点
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// 搜索脏entry并进行清理
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
// 如果向前未搜索到脏entry,则在查找过程遇到脏entry的话,后面就以此时这个位置
// 作为起点执行cleanSomeSlots
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
// 如果在查找过程中没有找到可以覆盖的entry,则将新的entry插入在脏entry
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
//执行cleanSomeSlots
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
调用ThreadLocal的get方法时,该方法主要是获取entry,同时遇到脏entry进行清理。
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
// 移除指定key的entry,同时进行脏entry清理
总结
1.ThreadLocal的解决hash冲突策略:开放地址法。
2.ThreadLocal的内存溢出:使用完ThreadLocal调用remove。
3.ThreadLocal的弱引用,当出现key为null(ThreadLocal被回收时),ThreadLocal的set,getEntry,remove都对key为null的entry进行了清理,使用弱引用的话在threadLocal生命周期里会尽可能的保证不出现内存泄漏的问题,达到安全的状态。
3.复杂点主要在清理entry的方法,包括expungeStaleEntry,cleanSomeSlots,replaceStaleEntry。