一、ThreadLocal简介
ThreadLocal常用于多线程环境下保证线程安全,它和synchronized以及lock略有不同,后两者是通过对访问权限的控制保证线程安全,而前者是通过增多资源的副本,保证当前线程操作自己所持有的副本,而不需要去与其它线程竞争。
二、ThreadLocal内部数据的存储
ThreadLocal将当前线程以及它操作的变量存储于一个ThreadLocalMap结构中,可以在ThreadLocal的set()方法中找到ThreadLocalMap,如下:
public void set(T value) {
Thread t = Thread.currentThread();//获取线程对象
ThreadLocalMap map = getMap(t);//查看当前线程是否已存在ThreadLocalMap
if (map != null)
map.set(this, value);//存在,值覆盖
else
createMap(t, value);//否则,创建一个当前线程的ThreadLocalMap
}
观察ThreadLocalMap的结构可以发现,它的key是WeakReference的即弱引用,而value是强引用。为什么要把key设置为弱引用呢?因为,由于ThreadLocalMap的生命周期和线程的生命周期相同,如果key设置为强引用的话,即使ThreadLocal执行结束,但只要线程周期未止,那么ThreadLocalMap对象将永远不会被回收,这样尤其容易发生内存溢出的问题。
三、弱引用的问题
是否使用了弱引用类型的key,ThreadLocal就可以完全避免OutOfMemoryError?下面做一个测试,首先修改虚拟机的最大堆空间为200M:
接着创建如下测试类,ThreadLocalOOM:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadLocalOOM {
private static final int THREAD_SIZE = 3000;//线程池大小
private static final int DATA_SIZE = 2000;//每个线程对应的变量所存储数据的数量
private static ThreadLocal<List<Integer>> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(THREAD_SIZE);
for (int i = 0; i < THREAD_SIZE; i++) {
service.execute(() -> {
threadLocal.set(new ThreadLocalOOM().insert());
System.out.println(Thread.currentThread().getName());
});
}
}
private List<Integer> insert() {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < DATA_SIZE; i++) {
list.add(i);
}
return list;
}
}
执行,在某个时段可能会看到如下结果,这里出现了OOM:
因此ThreadLocal并不能够完全避免OOM异常,那么它是怎么做的去减少OOM出现的概率呢?可以看到在ThreadLocal里面有一个remove()方法,如下:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());//如果当前线程有对应的ThreadLocalMap
if (m != null)
m.remove(this);//将会调用下面的方法
}
/**
* Remove the entry for key.
*/
//找到key对应的ThreadLocalMap,并清除entry数据
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
这样ThreadLocalMap中所持有的value也被释放掉,而key则会随着GC清除,避免了OOM的出现。
四、总结
通过上面得测试可知,当使用ThreadLocal的时候,应当显示的在适当的时候调用其remove方法,以避免虚拟机爆出OOM,除此之外,每一个ThreadLocalMap之中不应该存储太大的数据(防止某些必须与线程同生命周期的对象无法得到清除,导致OOM)。