每个Thread都有自己专属的ThreadLocalMap,其中包含了多个ThreadLocal和对应的value值。同时ThreadLocal又存在于多个Thread的ThreadLocalMap key中,每个ThreadLocalMap.get(threadLocal)获得的value值不一样,这样就实现了同一个ThreadLocal对应每个线程都有特殊的副本值。
threadLocal.set(value) 相当于 CurrentThread - > ThreadLocalMap -> put(threadLocal,value)
threadLocal.get() 相当于CurrentThread -> ThreadLocalMap -> get(threadLocal)
值得注意的是,虽然ThreadLocal存储了各个Thread的副本值,但是并不见得是线程安全的,当value是int这种基本类型的变量时,自然多线程操作不会影响。但当value是同一个对象 时,多线程同时操作,自然会出现线程安全问题。
public class ThreadLocalUnsafe implements Runnable {
private static Number num = new Number(0);
private static ThreadLocal<Number> tl = new ThreadLocal<Number>(){
@Override
protected Number initialValue() {
return new Number(0);
}
};
public static void main(String[] args) {
Thread[] arr = new Thread[5];
for(int i=0;i<5;i++){
Thread t = new Thread(new ThreadLocalUnsafe());
arr[i] = t;
}
System.out.println("==============");
Arrays.stream(arr).forEach(a->{a.start();});
}
@Override
public void run() {
num.setNum(num.getNum()+1);
tl.set(num);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+ num.getNum());
}
private static class Number{
int num;
public Number(int num){
this.num = num;
}
public void setNum(int num){
this.num = num;
}
public int getNum(){
return this.num;
}
}
}
因为多线程操作的是同一个static对象num,导致线程不安全。如果要避免这种情况发生,可以采用
内存泄漏指无法释放某些对象占用的空间。一般而言,JVM根据可达性分析判断堆对象是否有引用,如果没有引用,则在下次GC中进行回收。所以内存泄漏可以理解为,某些对象一直有引用,无法被回收。
引用又分为强、软、弱、虚四种,除了强引用存在时,堆对象一定无法被回收。但其他三种引用存在时,GC仍会遵循某种规律将对象进行回收。
下面这个例子设置-Xmx15m,最大堆内存15m。虽然每次都设置了abc=null,arr[]依然有指向new byte[]的强引用,新建的几个byte仍然可达,所以无法回收,内存泄漏最后大致内存溢出。
public class StrongReferer {
public static void main(String[] args) throws InterruptedException {
Object[] arr= new Object[5];
for(int i=0;i<5;i++){
Object abc = new byte[1024*1024*5];
arr[i]=abc;
System.out.println("创建第"+(i+1)+"个对象");
abc=null;
// arr[i]=null;
}
}
}
解决办法是每次循环结尾释放数组对byte[]的引用,arr[i]=null;
事实上Arraylist.clear()也是这么做的。
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
弱引用即使存在,也会在下次GC的时候回收对象。这里依然设置-Xmx15m,往数组里插入弱引用类型的ref变量,arr有指向ref堆对象的弱引用,即便可达,仍会在下次GC的时候被回收,程序不会内存溢出。
弱引用有效避免了内存泄漏的发生!
public class WeakReferer {
public static void main(String[] args) throws InterruptedException {
Object[] arr= new Object[5];
for(int i=0;i<5;i++){
Object str=new byte[1024*1024*5];
WeakReference<Object> ref = new WeakReference<Object>(str);
arr[i] =ref;
System.out.println("创建第"+(i+1)+"个对象");
}
System.out.println(arr);
}
}
但当程序运行到“创建第3个对象时”,发现arr[0]的referent已经为null了。
ThreadLocalMap中的entry中key,使用的是弱引用,好处是当指向ThreadLocal的强引用失效后,key指向的ThreadLocal弱引用会在下次GC时将key设置成null,然后ThreadLocalMap会在set(),get(),remove()时调用方法将该entry拿掉。这样就一定程度上避免了内存泄漏。
话不多说还是上例子,这里我们设置-Xmx30m,运行程序后发现不会溢出。但如果把arr[i]=tl的注释去掉,有了arr对tl堆对象的强引用,就无法回收内存,最终导致程序内存溢出。
public class ThreadLocalOOM2 extends Thread {
public static void main(String[] args) {
Object[] arr = new Object[8];
for(int i=0;i<8;i++){
System.out.println("set第"+(i+1)+"个对象");
ThreadLocal<byte[]> tl = new ThreadLocal<>();
tl.set(new byte[1024*1024*5]);
// arr[i]=tl;
System.gc();
}
}
}
那么为什么程序能GC回收ThreadLocal呢,自然要打断点进去一探究竟。
set第1个对象后,发现currentThread的ThreadLocalMap创建了新的Entry
在set第3个对象前,经过了System.gc(),再次观察ThreadLocalMap中的entry,发现原本指向第一个对象的ThreadLocal变为null了。
指向ThreadLocal堆对象的有两种引用
但这样就完了吗?value还存在呀!
只有把整个entry置为空,才能避免内存泄漏。幸运的是,threadLocalMap中set(),get(),remove()方法都会清除掉tab中key=null的entry。
(e.get() 是调用Reference.get()方法,获取referent即ThreadLocal)
至此,原先的tab[15]就被清除掉了,new byte[]没有任何引用也将被回收。
引用:https://www.jianshu.com/p/a1cd61fa22da
最终目的是要清除entry,回收value,防止内存泄漏。倒过来推,需要把key置为null,才会回收entry。key在什么情况下会被回收呢,只有在ThreadLocalMap对其是弱引用时,才会在没有强引用存在的情况下进行回收。
比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key的value就会导致内存泄漏,而不是因为弱引用。