一、ThreadLocal导致的内存泄露
1、根据前面的深入源码学习threadLocal(二)对ThreadLocal的分析,我们可以知道每个Thread 维护一个 ThreadLocalMap,这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object,也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。仔细观察ThreadLocalMap,这个map是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。
引用链如图所示:
这样,当把threadlocal变量置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收。这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而这块value永远不会被访问到了,所以存在着内存泄露。只有当前thread结束以后,current thread就不会存在栈中,强引用断开,Current Thread、Map value将全部被GC回收。最好的做法是不在需要使用ThreadLocal变量后,都调用它的remove()方法,清除数据。
代码演示:
/**
* 类说明:ThreadLocal造成的内存泄漏演示
*/
public class ThreadLocalOOM {
private static final int TASK_LOOP_SIZE = 100;
final static ThreadPoolExecutor poolExecutor
= new ThreadPoolExecutor(5, 5,
1,
TimeUnit.MINUTES,
new LinkedBlockingQueue<>());
static class LocalVariable {
private byte[] a = new byte[1024*1024*5];/*5M大小的数组*/
}
ThreadLocal<LocalVariable> localVariable;
//= new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
/*5*5=25*/
for (int i = 0; i < TASK_LOOP_SIZE; ++i) {
poolExecutor.execute(new Runnable() {
public void run() {
ThreadLocalOOM oom = new ThreadLocalOOM();
oom.localVariable = new ThreadLocal<>();
oom.localVariable.set(new LocalVariable());
System.out.println("use local varaible");
//如果我们执行任务完成之后不调用threadlocal的remove,那么占用内存为100M;如果调用的话,占用内存为25M
oom.localVariable.remove();
}
});
Thread.sleep(100);
}
System.out.println("pool execute over");
}
}
二、ThreadLocal导致的线程不安全的问题
1、在多线程情况下,尽量让每个线程中的ThreadLocal都应该持有一个新的Number对象。要不然会出现线程不安全的问题,代码演示:
public class ThreadLocalUnsafe implements Runnable {
//使用static修饰对象,静态变量被所有的对象所共享,在内存中只有一个副本。违反了我们ThreadLocal的原理。
// public static Number number = new Number(0);
//可以使用final字段修饰,或者是去掉static字段。
public final Number number = new Number(0);
public void run() {
//每个线程计数加一
number.setNum(number.getNum()+1);
//将其存储到ThreadLocal中
value.set(number);
SleepTools.ms(2);
//输出num值
System.out.println(Thread.currentThread().getName()+"="+value.get().getNum());
}
public static ThreadLocal<Number> value = new ThreadLocal<Number>() {
};
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new ThreadLocalUnsafe()).start();
}
}
private static class Number {
public Number(int num) {
this.num = num;
}
private int num;
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
@Override
public String toString() {
return "Number [num=" + num + "]";
}
}
}
我们发现ThreadLocalMap中保存的其实是对象的一个引用,这样的话,当有其他线程对这个引用指向的对象实例做修改时,其实也同时影响了所有的线程持有的对象引用所指向的同一个对象实例。这也就是为什么上面的程序为什么会输出一样的结果:5个线程中保存的是同一Number对象的引用,在线程睡眠的时候,其他线程将num变量进行了修改,而修改的对象Number的实例是同一份,因此它们最终输出的结果是相同的。