ThreadLocal用来在多线程环境中安全的保存某一个变量或对象,用以在当前线程的上下文传递。本文将介绍ThreadLoal的实现原理以及它的内存泄露问题,所以,首先会介绍JAVA中四种引用:强引用、软引用、弱引用、虚引用。
强引用、软引用、弱引用、虚引用
- 强引用:正常new出来对象就是强引用,当内存不够的时候,JVM宁可抛出异常,也不会回收强引用对象。
- 软引用(
SoftReference
):软引用生命周期比强引用低,在内存不够的时候,会进行回收软引用对象。软引用对象经常和引用队列ReferenceQueue
一起使用,在软引用所引用的对象被GC回收后,会把该引用加入到引用队列中。 - 弱引用(
WeakReference
):弱引用生命周期比软引用要短,在下一次GC的时候,扫描到它所管辖的区域存在这样的对象:一个对象仅仅被weak reference指向, 而没有任何其他strong reference指向,
,不管当前内存是否够,该对象都会被回收。弱引用和软引用一样,也会经常和引用队列ReferenceQuene
一起使用,在弱引用所引用的对象被GC回收后,会把该引用加入到引用队列中。 - 虚引用(
PhantomReference
):又叫幻象引用,与软引用,弱引用不同,虚引用指向的对象十分脆弱,我们不可以通过get方法来得到其指向的对象。它的唯一作用就是当其指向的对象变为不可达时,自己就被加入到引用队列,用作记录该引用指向的对象已被销毁。因此,无论对象是否覆盖了finalize方法,虚引用对象都没办法复活。
finallized方法: 当对象变成(GC Roots)不可达时(第一次回收),GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。因此,虚引用对象一旦变成不可达,便加入了引用队列,GC判断虚引用对象是否覆盖finalize方法时,发现虚引用已经加入了引用队列,自然就没办法将其放在F-Queue队列,而其他类型的对象一旦变成不可达,还可能再执行finalize方法,这就是为什么虚引用对象无法复活的原因
执行finalize方法完毕后,GC会再次判断该对象是否可达(第二次回收),若不可达,则进行回收,否则,对象“复活”。因此,对于重写了finallized方法的对象,会出现两个垃圾回收周期,这两个周期之间可能相隔了很久(取决于finalized方法执行是否及时),所以可能会出现大部分堆被标记为垃圾却还没有被回收,出现内存溢出的错误。
使用虚引用,上述情况将引刃而解,当一个虚引用加入到引用队列时,你绝对没有办法得到一个销毁了的对象。因为这时候,对象已经从内存中销毁了。因为虚引用不能被用作让其指向的对象重生,所以其对象会在垃圾回收的第一个周期就将被清理掉。
ThreadLocal
通常情况下,线程中对全局变量赋值后,可以被任何一个线程访问并修改的。
而创建全局变量ThreadLocal
,通过ThreadLocal
全局变量传递局部变量,该局部变量只能被当前线程访问,而且可以在线程的上下文传递,其他线程则无法访问和修改。
public class Test {
private final ThreadLocal<String> mystr = new ThreadLocal<>();
public void methodA() {
mystr.set("test_str_1");
}
}
实际上通过ThreadLocal
设置的值是放入了当前线程的一个ThreadLocalMap
实例中,所以只能在本线程中访问,其他线程无法访问。
实现原理
每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例,value是真正需要存储的Object。
从set()方法的实现,理解ThreadLocal实现
// 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()
方法:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
在Thread
类中,定义了两个属性,threadLocals
的初始化是在调用ThreadLocal
类中的getMap()
方法时完成的,当线程退出时,会将threadLocals
和inheritableThreadLocals
置为null。
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
对象。
ThreadLocalMap
是ThreadLocal
的静态内部类,是基于Entry数组的map。Entry
的key
是ThreadLocal
弱引用,目的是当线程退出时把threadLocal
实例置为null时,不再有强引用指向threadLocal
实例,不影响threadLocal
实例的垃圾回收。
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
的生命周期中,存在这些引用. 看下图: 实线代表强引用,虚线代表弱引用.
与上面的分析一致,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方法为例:
// 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
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
没有锁的机制,没有锁的开销