开篇
ThreadLocal主要解决的问题是给每个线程绑定自己的值,这个值是和线程绑定的,是线程的局部变量,是其他线程没法访问的。
ThreadLocal的源码的核心知识点在于ThreadLocal变量如何跟线程绑定和ThreadLocal如何实现gc垃圾回收,这篇文章希望能够讲解清楚这两个知识点。
ThreadLocal的实现原理:每一个线程在使用ThreadLocal的时候实际上是以ThreadLocal对象作为key值共享的对象为value值保存在Thread.threadLocalMaps变量中也就是ThreadLocalMap实例,因此ThreadLocal仅仅是作为一个key值来保存,多线程在使用同一个ThreadLocal或者不同的ThreadLocal但是保存相同的共享对象时他们的threadLocalMaps值是相同的,因此如果有共享资源发生冲突问题,ThreadLocal并不能解决!如果要解决并发冲突的问题要么使用安全对象,要么使用上锁机制来保证多线程顺序访问!
ThreadLocal的案例
通过下面例子可以对ThreadLocal的有一个直观的了解。
- 案例一:主线程main当中存在对象StudentInfo而子线程Thread-0当中不存在该变量,因为主线程通过set设置对象。
- 案例二:主线程main当中的对象StudentInfo通过set设置而子线程Thread-0当中的对象是初始化函数提供的,两者也不相同。
/** 创建一个ThradLocal实例 */
private static ThreadLocal<StudentInfo> threadLocal = new ThreadLocal<StudentInfo>();
public static void main(String[] args) {
StudentInfo info = new StudentInfo("sdew23", "张三", "男");
// 为主线程保存一个副本StudentInfo对象
threadLocal.set(info);
System.out.println(threadLocal.get());
System.out.println(Thread.currentThread().getName());
// 开启子线程
new Thread(new Runnable() {
@Override
public void run() {
// threadLocal.set(info);
System.out.println(threadLocal.get());
System.out.println(Thread.currentThread().getName());
}
}).start();
}

/** 创建一个ThradLocal实例 */
private static ThreadLocal<StudentInfo> threadLocal = new ThreadLocal<StudentInfo>() {
@Override
public StudentInfo initialValue() {
return new StudentInfo("sss", "小西", "女");
}
};
public static void main(String[] args) {
StudentInfo info = new StudentInfo("sdew23", "张三", "男");
// 为主线程保存一个副本StudentInfo对象
threadLocal.set(info);
System.out.println(threadLocal.get());
System.out.println(Thread.currentThread().getName());
// 开启子线程
new Thread(new Runnable() {
@Override
public void run() {
// threadLocal.set(info);
System.out.println(threadLocal.get());
System.out.println(Thread.currentThread().getName());
}
}).start();
}

ThreadLocal线程隔离的原理
ThreadLocal之所以能够实现线程隔离主要是因为在Thread的类变量当中存在一个ThreadLocal.ThreadLocalMap threadLocals类型对象,这样保证了每个线程能够单独维护一份ThreadLocal对象的map,也就自然而然实现了线程隔离。
ThreadLocal对象的set操作获取当前线程的ThreadLocal.ThreadLocalMap对象,以ThreadLocal对象作为key,以ThreadLocal保存的对象如下例中的StudentInfo为value,保存到ThreadLocalMap当中,每个线程执行set操作都是把对象保存至对应的ThreadLocalMap,所以也就解释了为什么能够隔离了。
ThreadLocal.ThreadLocalMap采用环形数组实现,也就是Entry[] table,set操作就是创建一个Entry对象然后放到环形数组table当中,通过线性探测方法解决冲突问题。
// 每个线程包含一个ThreadLocal.ThreadLocalMap对象
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
{
private static ThreadLocal<StudentInfo> threadLocal = new ThreadLocal<StudentInfo>();
StudentInfo info = new StudentInfo("sdew23", "张三", "男");
threadLocal.set(info);
}
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 getMap(Thread t) {
return t.threadLocals;
}
public class ThreadLocal<T> {
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
private void set(ThreadLocal<?> key, Object value) {
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) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
}
}

ThreadLocal的get操作
ThreadLocal的get操作从当前Thread获取ThreadLocal.ThreadLocalMap threadLocals对象,在ThreadLocalMap中以ThreadLocal对象作为key返回Entry也即ThreadLocal当中保存的value。
ThreadLocal的get操作内部考虑线性探测方法来解决存储时候的冲突问题,第一次以hash值取值成功则直接返回,如果未取到那么就通过线性探测法继续查找直到null值为止。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
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 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;
}
ThreadLocal的gc清理
ThreadLocal的key回收
ThreadLocal的垃圾回收机制是它的一大设计亮点,也是面试当中经常会被问到的问题,这里我们从两个维度进行说明。
- ThreadLocal当中的Entry的key实现了WeakReference弱引用,所以gc机制可以对key进行回收
- ThreadLocal当中的Entry的value由于没实现WeakReference弱引用,所以只有程序主动进行回收,在set/get操作中实现回收。
- 关于回收的细节可以参考文章《一篇文章,从源码深入详解ThreadLocal内存泄漏问题》,作者讲解的非常清楚给我很大启发。
// 关于key的弱引用实现机制
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocal的value回收-get阶段
getEntryAfterMiss过程当中会调用expungeStaleEntry()方法删除过期数据,判断过期数据的标准是对Entry.get()方法返回的key为null(被jvm的gc主动回收了),这个时候需要把对应的value也进行删除。
ThreadLocal的expungeStaleEntry()方法内部除了删除staleSlot指定的过期数据外,还负责检查往后遍历直至遇到数组元素为null停止。在删除过程中还会对某些未过期的数据进行重hash,我认为之所以重hash有可能之前通过线性探测法往后放的,通过重hash后如果发现原来位置为空则可以直接放置hash值对应的下标位置。之所以这么做我个人觉得是因为get或者set的依据都是以null作为结束依据。
get时候重hash是为了把元素放置到hash值第一次对应的位置当中(未经过线性探测法解决碰撞冲突),这样就可以支持以null作为结尾的依据了。
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 int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
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;
}
ThreadLocal的value回收过程
cleanSomeSlots以i作为起始地址,expungeStaleEntry()完成i位置的元素的gc回收,然后继续遍历回收直至遇到null元素的下标i,然后由cleanSomeSlots继续nextIndex()下移到下一个位置继续查找。
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);
}
} while ( (n >>>= 1) != 0);
return removed;
}
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
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;
}

说明:
1、如图当前n等于hash表的size即n=10,i=1,在第一趟搜索过程中通过nextIndex,i指向了索引为2的位置,此时table[2]为null,说明第一趟未发现脏entry,则第一趟结束进行第二趟的搜索。
2、第二趟所搜先通过nextIndex方法,索引由2的位置变成了i=3,当前table[3]!=null但是该entry的key为null,说明找到了一个脏entry,先将n置为哈希表的长度len,然后继续调用expungeStaleEntry方法,该方法会将当前索引为3的脏entry给清除掉(令value为null,并且table[3]也为null),但是该方法可不想偷懒,它会继续往后环形搜索,往后会发现索引为4,5的位置的entry同样为脏entry,索引为6的位置的entry不是脏entry保持不变,直至i=7的时候此处table[7]位null,该方法就以i=7返回。至此,第二趟搜索结束;
3、由于在第二趟搜索中发现脏entry,n增大为数组的长度len,因此扩大搜索范围(增大循环次数)继续向后环形搜索;
4、直到在整个搜索范围里都未发现脏entry,cleanSomeSlot方法执行结束退出。