简介
java.lang.ThreadLocal类,自JDK1.2版本就加入了Java,ThreadLocal类可以给每个线程维护一个独立的变量副本,使多线程的场景使用共有的ThreadLocal变量,同时每个线程在ThreadLocal对象中保存的变量副本是相互隔离的。
调用ThreadLocal的
public void set(T value)
方法,就可以为当前线程设置一个线程专有的变量。
调用ThreadLocal的
public T get()
方法,可以获得当前线程专有的变量。
下面是一个简单的使用ThreadLocal的例子:
static class MyThread extends Thread {
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
@Override
public void run() {
super.run();
for (int i = 0; i < 3; i++) {
threadLocal.set(i);
System.out.println(getName() + " threadLocal.get() = " + threadLocal.get());
}
}
}
public static void main(String[] args) {
MyThread myThreadA = new MyThread();
myThreadA.setName("ThreadA");
MyThread myThreadB = new MyThread();
myThreadB.setName("ThreadB");
myThreadA.start();
myThreadB.start();
}
输出的结果是:
ThreadA threadLocal.get() = 0
ThreadA threadLocal.get() = 1
ThreadB threadLocal.get() = 0
ThreadA threadLocal.get() = 2
ThreadB threadLocal.get() = 1
ThreadB threadLocal.get() = 2
可以看到,虽然两个线程使用了一个ThreadLocal的静态变量,但是get()方法得到的值都是0,1,2三个值,说明两个线程在set值的时候值是隔离的。
通过get()和set()方法的源码,我们可以知道ThreadLocal维护了怎样的数据结构,从了解ThreadLocal如何使线程间的变量相互隔离。
ThreadLocal的get()方法:
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();
}
首先拿到当前线程,然后把当前线程当参数传给了getMap()方法:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
返回的是当前线程的threadLocals属性。下面就是ThreadLocal数据结构中最绕的一部分, Thread和ThreadLocal两个类共同搭建了一套线程隔离系统。
Thread类中threadLocals属性的定义是这样的:
ThreadLocal.ThreadLocalMap threadLocals = null;
这是一个在ThreadLocalMap中定义的静态内部类,是个Map,里面维护了一个Entry类的数组。这个Entry是在ThreadLocalMap类中定义的内部类:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
可以看到这个Entry继承WeakReference类,对key使用的是弱引用。
回到之前的get()代码,获得Thread中的ThreadLocalMap后,判断是否存在。
如果ThreadLocalMap不存在则调用setInitialValue()方法初始化。
如果ThreadLocalMap存在,则从map中获得Entry,用的key就是当前ThreadLocal对象,也就是这一行:
ThreadLocalMap.Entry e = map.getEntry(this);
然后从Entry中拿到value,就是get()方法要的结果。
ThreadLocal的set()方法:
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,然后set进去。如果ThreadLocalMap还没创建,则调用createMap()方法创建。
get()方法中的setInitialValue()方法和set()方法中的createMap()方法都是创建ThreadLocalMap,而且创建完了都往里面写了一个Entry对象,key都是ThreadLocal对象,区别在于setInitialValue()方法中设置的value是ThreadLocal配置的默认值,createMap()方法中设置的value就是我们在set()方法中传的值。
综上可知,每个线程都维护了一个ThreadLocalMap,ThreadLocalMap中 有Entry数组,Entry的key是ThreadLocal对象,Entry的value就是线程隔离的值。
ThreadLocal的数据结构不太好理解,主要是因为这个类里set()的值实际上是二维的,就像二维数组的值一样,下面的图有助于理解ThreadLocal维护了一个什么数据结构:
所以,这个数据结构反过来就好理解一些,实际上是每个线程维护了一个map,而每个ThreadLocal对象就是map的key。
关于内存泄露:
上图的虚线表示弱引用。
以下这些是强引用:
1,ThreadLocalMap对Entry的引用。
2,Entry对value的引用。
注意到set()方法中有这么一行:
map.set(this, value);
ThreadLocalMap的set()方法是这样的:
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because i
// least as common to use set() to create new entri
// it is to replace existing ones, in which case, a
// 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();
}
可以看到,当ThreadLocalMap打算往Entry里set一个值,循环数组的时候顺便还判断了一下每个Entry的key值,如果是key是null的话就调用replaceStaleEntry()方法:
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// Find either the key or trailing null slot of run, whichever
// occurs first
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
if (slotToExpunge == staleSlot)
slotToExpunge = i;
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.
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
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(expungeStaleEntry(slotToExpunge), len);
}
这个方法可以把key是null的Entry的value设为null,从而释放对value对象的引用。
在ThreadLocalMap的set()方法的最后,调用了rehash()方法:
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
方法本身是用来扩容的,但是方法开始的时候调用的expungeStaleEntries()方法会把key是null的Entry设为null,释放对Entry的引用:
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
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的get()方法中也调用了这个expungeStaleEntries()方法。说明get()/set()方法都能帮助缓解内存泄露,ThreadLocal对象被回收后,各线程中的Entry,Entry中的value,都可以被回收,防止内存泄露。
如果一直不调用get()和set()方法那上面的套路就无效了,所以,手动调用ThreadLocal的remove()方法,是比较靠谱的做法。
完