ThreadLocal详细介绍+源码分析
前言:本篇文章要研究的是ThreadLocal 这个类,基于JDK1.8进行源码分析。ThreadLocal 主要用来提供线程局部变量,也就是变量只对当前线程可见。这个类也是面试中出现率很高的问题,特此整理一番。
一、ThreadLocal源码分析
1.1 什么是ThreadLocal
首先,它是一个数据结构,有点像HashMap,可以保存”key : value”键值对,但是一个ThreadLocal只能保存一个,并且各个线程的数据互不干扰。
ThreadLocal<String> localName = new ThreadLocal<>();
localName.set("threadlocal");
String name = localName.get();
在线程1中初始化了一个ThreadLocal对象localName,并通过set方法,保存了一个值threadlocal,同时在线程1中通过localName.get()可以拿到之前设置的值,但是如果在线程2中,拿到的将是一个null。 因为如前言中提到的,ThreadLocal存储的变量只对当前线程可见。
1.2 源码起飞
那么这是如何实现的呢?让我们进入美滋滋的欣赏源码环节:
首先来看set()
方法:
public void set(T value) {
// 得到当前线程对象
Thread t = Thread.currentThread();
//获取当前线程中的ThreadLocalMap
ThreadLocalMap map = getMap(t);
//如果map存在,则将当前ThreadLcoal对象作为key,要存储的对象作为value存到map里面去
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
note:
map.set(this,value)
由于是在ThreadLocal
类中调用,所以this
指的就是ThreadLocal
对象。
接下来看getMap(Thread t)
方法:
//就是返回线程中的threadLocals
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
我们进到threadLocals的声明处,他是在Thread类下:
ThreadLocal.ThreadLocalMap threadLocals = null;
这时我们遇到了另一个类ThreadLocalMap
。
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
........省略其它
}
通过查看源码我们可以发现的是ThreadLocalMap
是ThreadLocal
的一个内部类。 用Entry
类来进行存储。 我们的值都是存储到这个Map上的,key
是当前ThreadLocal
对象!上面还涉及到WeakReference<T>
(弱引用),有关弱引用见相关博客。
继续往下走,如果map不为空,则把值放入map
中。看一下set(ThreadLocal<?> key, Object value)
方法。
这个方法是由map调用,故为ThreadLocalMap
类中的方法:
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) {
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();
}
这个方法的作用就是把key
(当前的ThreadLocal对象)和value
(我们要存储的值)存入Entry[] tab
中。
继续往下看,如果map
为空,则创建一个map
。即调用createMap(Thread t, T firstValue)
方法。
该方法是ThreadLocal
类中的方法。源码如下:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
又调用到了ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)
方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
//INITIAL_CAPACITY=16 必须是2的整数倍
同时又调了setThreshold(int len)
方法;
//使resize的阈值为2/3的负载因子
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
所以我们知道会有resize
这样的方法来扩充table
的容量。也确实有:
//使table的容量变为原来的2倍
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
我们想知道谁调用了resize()
呢。通过查看调用关系(Eclipse下的快捷键是Ctrl+Alt+H),发现是rehash()
方法调用的resize()。
/**
* Re-pack and/or re-size the table. First scan the entire
* table removing stale entries. If this doesn't sufficiently
* shrink the size of the table, double the table size.
*/
private void rehash() {
//擦除陈腐的Entries
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
那谁又调了rehash()
呢,我们发现是老熟人set(ThreadLocal<?> key, Object value)
。至此恍然大悟,当我们向map
中set数据时,最终存到的是ThreadLocal
的内部类ThreadLocalMap
中的Entry[] table
中。并且这个table
有扩容和清理无用对象的机制。
1.3 小总结
- 每个Thread维护着一个ThreadLocalMap的引用。
- ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储。
- 调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值是传递进来的对象。
- 调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象。
- ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。
二、避免内存泄漏
我们来看一下ThreadLocal的对象关系引用图:
001.png
ThreadLocal
内存泄漏的根源是:由于ThreadLocalMap
的生命周期跟Thread
一样长,如果没有手动删除对应key,就会导致value永远访问不到,从而导致内存泄漏。
想要避免内存泄露就要手动remove()掉!
参考资料: