ThreadLocal
从字面上就可以看出,ThreadLocal是属于线程的私有变量,只有当前线程才可以访问。其次,ThreadLocal主要是对线程Thread的ThreadLocalMap进行操作,以ThreadLocal为键值向其中保存数据。
用法
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("test-value");
String str = threadLocal.get();
System.out.println(str);
实现原理
ThreadLocal的使用方法比较简单,主要是set()和get()方法,下面我们去ThreadLocal的源码中寻找答案。
set
public void set(T value) {
// 获取当前线程引用
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value); // set值
else
createMap(t, value);
}
/**
* 通过当前线程获取map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
我们发现set操作很大程度上都与ThreadLocalMap有关,接下来我们需要了解ThreadLocalMap的实现原理,ThreadLocalMap是ThreadLocal中的一个静态内部类:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table; // Entry数组,用来存放键值对
/**
* 构造方法
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY]; //初始化Entry数组
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //通过key的hashcode计算在数组中的位置
table[i] = new Entry(firstKey, firstValue); //保存到数组中
size = 1;
setThreshold(INITIAL_CAPACITY);
}
/**
* Set the value associated with key.
*/
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 因为Entry中没有next指针,所以不能用链表的方式解决key冲突,这里使用nextIndex(i, len)来解决键值的冲突,如果出现冲突,向后移固定长度再次检查key
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 找到key,重置数据
if (k == key) {
e.value = value;
return;
}
// 如果key不存在,用新的Entry对象填充
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
}
上面是ThreadLocalMap的一些细节,ThreadLocalMap类中又包含了实体类Entry,Entry中主要包含ThreadLocal类型的键值和Object类型的数据,而ThreadLocalMap中维护了一个数组:Entry[],用于保存键值对,但是Entry中没有HashMap中的next指针,所以无法使用链表的形式解决冲突,这里ThreadLocalMap通过轮询数组中的元素进行安插键值对。
get
public T get() {
// 通过当前线程获取ThreadLocalMap
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 通过ThreadLocalMap和ThreadLocal对象获取数据
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
我们再看一下get()方法中获取Entry的map.getEntry(this)方法,ThreadLocalMap.getEntry(ThreadLocal> key):
“`
private Entry getEntry(ThreadLocal<?> key) {
// 根据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); //key值发生冲突,在数组下面继续找
}
<pre><code class="line-numbers"><br />### 原理总结 ###
由上诉分析我们知道,每个线程都维护了一个ThreadLocalMap对象,ThreadLocalMap中又维护了一个元素为Entry的数组,Entry为键值对的实体对象,其中key为ThreadLocal对象,值为Object类型数据。但是与HashMap不同的是数组中的元素没有next指针,不是通过链表来解决冲突的。所以我们也可以得知一个ThreadLocal对象可以类比HashMap中的一个Entry对象,她只能保存一个键值对,其中键值就是ThreadLocal对象本身。
### 内存泄漏 ###
如果我们使用线程池维护线程,线程中创建了大对象到ThreadLocal中,但是在线程完成工作后线程未必会退出,而可能仍然维护在线程池中等待下一次调用,如果没有显式地使用ThreadLocal.remove()方法将其移除,那么ThreadLocal仍然可以保存下来,但是此时这个变量已经没有任何用途,就可能使系统发生内存泄漏。
然后我们考虑另外一种情况:因为ThreadLocalMap.Entry中的key是“弱引用”,即
</code></pre>
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // 调用WeakReference包装成弱引用
value = v;
}
}
“`
Entry中的key是外部ThreadLocal对象的弱引用,而虚拟机GC时一发现弱引用就会立即回收,如果ThreadLocal对象的外部强引用被回收时(此时只剩下Entry中key对其的弱引用),在下次发生GC时就会回收被弱引用的key,ThreadLocalMap的key就会变成null,但是key对应的Entry中的value仍保存在ThreadLocalMap中,一直得不到回收,造成内存泄漏。
Entry中的key为什么使用弱引用
我们知道Entry是ThreadLocalMap中的元素,Entry的key被设置为弱引用,但是为什么不能是强引用呢?
试想当我们想要清除ThreadLocalMap中的一个ThreadLocal对象,如果仅仅将ThreadLocal对象的强引用设为null时,在GC时仍无法回收ThreadLocal对象的内存,因为在ThreadLocalMap.Entry中还保留着对ThreadLocal对象的强引用,所以,Entry需要将ThreadLocal的引用设置为弱引用,当外部强引用消除后只剩下弱引用,在后续的GC中可以立即被回收。
参考
《Java高并发程序设计》–葛一鸣、郭超