最近从网上看到一个关于ThreadLocal的问题,
TheadLocal是否会导致内存溢出?
从理论上说是会的,但是要看怎么使用,既然Java设计了这个东西,肯定是有考虑过它的很多使用场景的,所以大部分情况下其实还是可以放心使用的。
在回答会不会导致OOM之前我们先来了解一下什么是ThreadLocal?通过英文字义,很容易就可以猜到它作用就是用来保存线程的本地变量。
1.下面我们来分析一下ThreadLocal的原理
我们先来看ThreadLocal的get(),set()方法的源码(通过源码去分析它的原理是最直接的)
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
//把this(ThreadLocal)当作map的Key
map.set(this, value);
else
createMap(t, value);
}
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
//把this(ThreadLocal)当作Key,拿到对应的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
//从当前的线程对象里面拿出ThreadLocalMap对象
return t.threadLocals;
}
通过源码可以发现,TheadLocal.set的value保存的位置不是在TheadLocal里面,而是在当前线程的ThreadLocalMap里面,并且ThreadLocalMap的key是ThreadLocal本身。当调用get()的通过传入ThreadLocal本身就可以拿到对应的Entry。接下来我们看到Entry的实现,它继承了WeakReference类
(WeakReference是Java的弱引用,每次调用GC的时候如果某个对象只存在弱应用,那么它一定会被回收掉)。
下面是ThreadLocalMap 的Entry 的实现
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
。。。
}
在Entry中,将key传入了WeakReference的构造函数,也就是说只有Key是弱引用,value是强引用,而threadLocalMap对Entry的引用也是强引用。(如下图)
由于Key是弱引用,也就是说,当threadLocalMap中的key只要在外面不存在任何强引用的情况threadLocalMap中的key是会被回收的,那key回收了之后有什么作用呢?我们再来看一下threadLocalMap的set方法实现
private void set(ThreadLocal<?> key, Object value) {
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;
}
//当key为null时,将key为null的entry的引用去掉
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
通过上面的代码,我们可以看出来,调用set方法时每次都会去查询threadLocalMap的entry,如果存在key为空时,它的entry的将会被清除掉。(调用threadLocalMap的get,remove方法也会)
所以通过上面的代码分析之后,我们得出了两个结论:
1.threadlocal变量是缓存在thread对象里面,threadlocal只是做key来使用,那么如果thread给回收了,那么threadlocal的变量缓存也会被回收。
2.threadLocalMap对threadlocal采用的是弱引用, threadlocal在外面不存在强引用时,只要有其他的方法调用了的threadLocalMap的get,set或remove方法,它会清除掉一些已经无效的threadlocal的缓存。
我们知道要导致OOM就得必须不停的创建对象,并且创建的对象不会被回收掉。那么要threadlocal的缓存不被回收,那需要满足以下两点:
1.首先调用Threadlocal的线程,寿命必须要有足够长(这个是有可能的,很多线程池的线程都会重复使用)
2.如果只是一个threadlocal对象是很难导致OOM的,假如我们的应用程序有1000个线程,那最多这个threadlocal对象使用的本地缓存变量最多也1000个,现在的服务器那么多的内存,这个占用还不至于导致内存泄漏,所以我们需要非常泛滥的使用threadlocal变量,而且必须是静态的,因为静态变量的引用才会永久存在。
分析完了上面的原理之后,我们再来分析一下实际中使用的场景
场景1
class BizContext {
private ThreadLocal<Object> local = new ThreadLocal<>();
public void setVal(Object val) {
local.set(val);
}
public Object getVal() {
return local.get();
}
}
class TestBiz1 {
BizContext context;
public TestBiz1(BizContext context) {
this.context = context;
}
public void doSomething() {
BizContext ctx = this.context;
Random random = new Random();
for (int i = 0; i < 1000; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
ctx.setVal(random.nextInt());
TestBiz2 testBiz2=new TestBiz2(context);
testBiz2.doSomething();
}
});
thread.start();
}
}
}
class TestBiz2 {
BizContext context;
public TestBiz2(BizContext context) {
this.context = context;
}
public void doSomething() {
System.out.println(context.getVal());
}
}
class Test {
public static void main(String[] arr) {
Random random = new Random();
for (int i = 0; i < 1000000; i++) {
BizContext context = new BizContext();
TestBiz1 testBiz1 = new TestBiz1(context);
testBiz1.doSomething();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
这种场景不会导致内存溢出,为什么呢?
1.首先上面例子每次调用testBiz1.doSomething(),都会创建1000个线程,但是线程的寿命都很短,线程使用完之后就会被回收(我们先不管这样使用性能的好坏),所以ThreadLocal的缓存也会跟着一起回收。
2.在main方法的for循环里面,每次调用调用完testBiz1.doSomething()方法之后,BizContext 都是新实例化的,旧的BizContext 实例它没有被任何对象引用,所以它和里面的ThreadLocal都会被回收掉,所以即使调用的线程即使调用完之后没有死,在调用它的ThreadLocalMap的set方法时,引用旧的ThreadLocal的entry也会被回收掉。
场景二
class BizContext {
public static ThreadLocal<Object> local1=new ThreadLocal<>();
public static ThreadLocal<Object> local2=new ThreadLocal<>();
public static ThreadLocal<Object> local3=new ThreadLocal<>();
public static ThreadLocal<Object> local4=new ThreadLocal<>();
public static ThreadLocal<Object> local5=new ThreadLocal<>();
public static ThreadLocal<Object> local6=new ThreadLocal<>();
public static ThreadLocal<Object> local7=new ThreadLocal<>();
....
public static ThreadLocal<Object> local10000=new ThreadLocal<>();
}
class Test{
public static void main(String[] arr) {
ExecutorService executorService = new ThreadPoolExecutor(1000, 1000,
0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000000),
new ThreadPoolExecutor.DiscardPolicy());
}
for (; ; ) {
Person person = new Person();
executorService.execute(() -> {
BizContext.local1.set(1);
BizContext.local2.set(2);
BizContext.local3.set(3);
BizContext.local4.set(4);
BizContext.local5.set(5);
......
BizContext.local10000.set(10000);
});
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上面这种场景模拟的是ThreadLocal变量泛滥的情况 (虽然有点夸张,没有人会可能这样用,但是这里只是为了举例),这种是可能会发生内存溢出的
结论
ThreadLocal的设计是非常合理的,并不是说用了它就一定会导致内存溢出,正常的使用是不用担心的。