1. 写在前面
LZ原先对于ThreadLocal的了解,仅限于它内部是一个以当前线程为键的map,但查看源码发现键是ThreadLocal对象本身。今天终于彻底看了看它的内部原理,故写此文以便以后复习学习。
2. ThreadLocal相关类介绍
为了理解ThreadLocal类的工作原理,必须同时介绍与其工作甚密的其他几个类与方法。
- ThreadLocalMap(内部类,存储value)
ThreadLocalMap中关于entry的定义:ThreadLocalMap.Entry.png 从中可以发现Map的key是ThreadLocal,值是用户的值,并不是原先认为的以当前线程为键。值存在Entry内,而键存在了WeakReference内,WeakReference为弱引用对象,这里与ThreadLocal内存泄漏有一定关系。
-
Thread(使用ThreadLocalMap)
在Thread有一行代码:
Thread.png ThreadLocalMap定义在ThreadLocal中,引用是在Thread中。
- set(),get(),getMap(),createMap()
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
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();
}
ThreadLocalMap getMap(Thread t) {
// ThreadLocalMap在当前线程被所有ThreadLocal共享
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
// 初始化map,构建table与Enrty
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
看到这里,可以明白大致的流程了,小小的总结一下:
- ThreadLocalMap用来存储用户的value,这个map的引用在Thread类里,是全线程唯一的。
- 当set时,首先获取全线程唯一的ThreadLocalMap,key是ThreadLocal对象,get时类似。
- ThreadLocal不是用来解决多线程并发访问异常的,因为每一个线程的ThreadLocalMap都不是同一个;并且如果向ThreadLocal存入同一个对象,还是会存在并发访问异常,下面给出一个例子
public class Son implements Cloneable {
public static void main(String[] args) {
final Son p = new Son();
Thread t = new Thread(new Runnable() {
public void run() {
ThreadLocal<Son> threadLocal = new ThreadLocal<Son>();
System.out.println(threadLocal);
threadLocal.set(p);
System.err.println("克隆前: " + threadLocal.get());
threadLocal.remove();
try {
threadLocal.set((Son) p.clone());
System.err.println("克隆后: " + threadLocal.get());
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
System.out.println(threadLocal);
}
});
t.start();
}
}

所以,ThreadLocal的作用是在同一个线程周期内,将变量在不同的方法或者模块中进行数据传递,或者通过放入clone对象,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。
3. 每一个ThreadLocal对象是如何区分的
查看源码,可以看到
//java提供的,可以用原子方式更新的 int值的类。
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
//原子性加一
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
对于每一个ThreadLocal对象,都有一个final的int值threadLocalHashCode;AtomicInteger 是static修饰的,全局唯一,每一次加一之后的值仍然可用,并且保证原子性。所以,每一个线程的ThreadLocal对象都有唯一的threadLocalHashCode值。
4. 为什么不使用当前线程作为key?
上面知道,每一个线程周期,都有一个全线程唯一的map用于存储value,如果线程内多个ThreadLocal对象set了value,那么以当前线程作为键是不能保证key的唯一性的;而每一个ThreadLocal对象都可以由threadLocalHashCode进行唯一区分,所以key使用为ThreadLocal方便存取。
5. ThreadLocal的内存泄露问题
通过上面的Entry源码,发现ThreadLocal的键是弱引用,下图中实现是强引用,虚线是弱引用。
如果ThreaLocal对象没有一个强引用,那么当gc时,ThreadLocal对象会被回收,ThreadLocalMap内Entry的key就变成null,但是enrty本身还是有一个强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,如果线程迟迟没有死亡,那么永远无法回收,造成内存泄漏。

- 首先从ThreadLocal的找到索引位置(通过ThreadLocal.threadLocalHashCode & (table.length-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e;
- 如果e为null或者key不一致则向数组table的下一个位置查询,如果发现相等,则返回对应的Entry。否则,如果key值为null,则擦除该位置的Entry,并继续向下一个位置查询。在这个过程中遇到的key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,自然会被回收。set操作也有类似的思想,将key为null的这些Entry都删除,防止内存泄露。
但是这些操作的前提是调用set方法或者getEntry方法,所以JAVA官方推荐将ThreadLocal定义为static全局唯一,避免丢失ThreadLocal强引用,就能保证随时remove调entry内的key与value。
网友评论