threadLocal原理

强引用、软引用、弱引用、虚引用

在了解threadLocal之前,有必要了解JAVA中的四种引用:

  • 强引用:正常new出来对象就是强引用,当内存不够的时候,JVM宁可抛出异常,也不会回收强引用对象。
  • 软引用(SoftReference):软引用生命周期比强引用低,在内存不够的时候,会进行回收软引用对象。软引用对象经常和引用队列ReferenceQueue一起使用,在软引用所引用的对象被GC回收后,会把该引用加入到引用队列中。
  • 弱引用(WeakReference):弱引用生命周期比软引用要短,在下一次GC的时候,扫描到它所管辖的区域存在这样的对象: 一个对象仅仅被weak reference指向, 而没有任何其他strong reference指向,,不管当前内存是否够,该对象都会被回收。弱引用和软引用一样,也会经常和引用队列ReferenceQuene一起使用,在弱引用所引用的对象被GC回收后,会把该引用加入到引用队列中。
  • 虚引用(PhantomReference):又叫幻象引用,与软引用,弱引用不同,虚引用指向的对象十分脆弱,我们不可以通过get方法来得到其指向的对象。它的唯一作用就是当其指向的对象将被回收时,自己被加入到引用队列,用作记录该引用指向的对象即将被销毁。

finallized方法: 当对象变成(GC Roots)不可达时(第一次回收),GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达(第二次回收),若不可达,则进行回收,否则,对象“复活”。因此,对于重写了finallized方法的对象,会出现两个垃圾回收周期,这两个周期之间可能相隔了很久(取决于finalized方法执行是否及时),所以可能会出现大部分堆被标记为垃圾却还没有被回收,出现内存溢出的错误。

使用虚引用,上述情况将引刃而解,当一个虚引用加入到引用队列时,你绝对没有办法得到一个销毁了的对象。因为这时候,对象已经从内存中销毁了。因为虚引用不能被用作让其指向的对象重生,所以其对象会在垃圾回收的第一个周期就将被清理掉。

ThreadLocal

通常情况下,线程中对全局变量赋值后,可以被任何一个线程访问并修改的。

而创建全局变量ThreadLocal,通过ThreadLocal全局变量传递局部变量,该局部变量只能被当前线程访问,而且可以在线程的上下文传递,其他线程则无法访问和修改。

1
2
3
4
5
6
7
public class Test {
private final ThreadLocal<String> mystr = new ThreadLocal<>();

public void methodA() {
mystr.set("test_str_1");
}
}

实际上通过ThreadLocal设置的值是放入了当前线程的一个ThreadLocalMap实例中,所以只能在本线程中访问,其他线程无法访问。

ThreadLocal的实现原理

每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例,value是真正需要存储的Object。

从set()方法的实现,理解ThreadLocal实现

1
2
3
4
5
6
7
8
9
// jdk1.8 source code 
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

在调用set方法时

  • 首先获取当前线程 Thread.currentThread()
  • 利用当前线程获取一个ThreadLocalMap对象
  • 判断map是否为空,若为空,创建这个ThreadLocalMap对象并设置值,不为空,则设置值。

getMap()方法:

1
2
3
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

Thread类中,定义了两个属性,threadLocals的初始化是在调用ThreadLocal类中的getMap()方法时完成的,当线程退出时,会将threadLocalsinheritableThreadLocals置为null。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ThreadLocal.ThreadLocalMap threadLocals = null; // ThreadLocalMap对象
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; // 子类可继承的ThreadLocalMap对象

// 线程退出后,将threadLocals和inheritableThreadLocals置为null
private void exit() {
if (group != null) {
group.threadTerminated(this);
group = null;
}
/* Aggressively null out all reference fields: see bug 4006245 */
target = null;
/* Speed the release of some of these resources */
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}

现在,完成了前两步,获取当前线程的ThreadLocalMap对象。

ThreadLocalMapThreadLocal的静态内部类,是基于Entry数组的map。EntrykeyThreadLocal弱引用,目的是当线程退出时把threadLocal实例置为null时,不再有强引用指向threadLocal实例,不影响threadLocal实例的垃圾回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
}
}

threadlocal的生命周期中,存在这些引用. 看下图: 实线代表强引用,虚线代表弱引用.

img

与上面的分析一致,Entry的key为弱引用,它的引用链是ThreadLocalRef -> ThreadLocal ---> key,当栈中的ThreadLocalRef与堆中的ThreadLocal断开时,ThreadLocal实例就会被垃圾回收。

value为强引用,它的引用链是CurrentThreadRef -> CurrentThread -> ThreadLocalMap -> Entry -> value,只要当前线程没有关闭,CurrentThreadRef -> CurrentThread的引用就不会断开,value就不会被垃圾回收。只有当前thread结束以后, CurrentThread就不会存在栈中,强引用断开, CurrentThread, Map, value将全部被GC回收.

是否存在内存泄露?

上节提到当前线程没有退出,将会一直存在CurrentThread至value的引用链,即便将threadLocal手动设置为null也依然存在CurrentThread至value的引用链。这会给开发者产生一种内存泄露的错觉(错觉:value是通过threadLocal设置的,我明明将threadLocal设置为了null,为什么value还会占用内存?),尤其在使用线程池时更容易出现这样的错觉,因为线程池的线程结束后,会放回线程池中不销毁。

可以理解为:threadLocal没有内存泄露,泄露的是Entry。

JDK的优化

为了减缓这种错觉的产生,Java会在调用threadLocal实例的get、set方法且key为null时,清除Entry。以get方法为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// threadlocal.get()
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); //此处调用threadlocalMap.getEntry()
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

// threadlocalMap.getEntry()
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); // 没找到该key(threadlocal)时,调用该方法
}

// hash未命中时调用该方法
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;

while (e != null) { // ThreadRef这条链还没断,thread未被销毁,entry不为Null
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null) // threadLocalRef这条链已断开,threadLocal实例为Null
expungeStaleEntry(i); // 删除所有key为null的Entry
else
i = nextIndex(i, len);
e = tab[i];
}

return null;
}

不仅在调用get方法,在调用set、remove方法时,threadLocal为null时,也会最终调用到expungeStaleEntry()方法 ,清除所有threadLocal为null时entry的强引用,这里不赘述了。

因此,正确的使用方式是,首先判断是否存在场景:threadLocal置为null?

如果存在,在调用完set、get后,记得调用remove方法显示的清除Entry的强引用。如果不存在,threadLocal一直在使用,没有被回收的必要,也不care脏读的情况,那更没必要去回收threadLocalMap中的Entry了。

脏读

示例如下,创建一个大小为8的线程池,向该线程池提交100次任务,因为使用的是线程池,线程不会被销毁,所以假设某一个线程写入了值,然后该线程处于空闲态,然后该线程再次读取时,读取到的是上次该线程运行时设置的值。

可能下面的例子很明显就看的出问题所在,但是当项目复杂时,在多处调用get,就比较容易出现这种问题。

不过这种情况也很容易避免,有两种方法:

  • set、get成对出现,set在前、get在后
  • 使用remove
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test {
private final ThreadLocal<String> mystr = new ThreadLocal<>();

public void methodA() {
ExecutorService executor = Executors.newFixedThreadPool(8);
for (int i=0; i<100; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
if (i % 4 == 0) {
String s = mystr.get();
}
mystr.set("test"+i);
}
});
}

}
}

Hash碰撞

在某个线程中,每new一个ThreadLocal实例,该线程的ThreadLocalMap中就会新增的一个key,当ThreadLocal实例过多时,自然会出现hash碰撞。

HashMap的最大的不同在于,ThreadLocal.ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表/红黑树的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。所以在开发的过程中,要避免这一点,提高运行效率。

与synchronized的区别

  • ThreadLocal用于处理线程内部上下文变量的传递,变量不会被其他线程访问,而synchronized修饰的变量,只要其他线程获取了锁,就能访问、修改
  • ThreadLocal没有锁的机制,没有锁的开销