玩碎Java之ThreadLocal的原理

ThreadLocal,我们依然从一个例子看起。

例子

public class ThreadLocalTest2 {

    public static void main(String[] args) {
        // ①,强引用tl1
        ThreadLocal tl1 = new ThreadLocal();
        tl1.set(11);
        System.gc();
        tl1.get();
        System.gc();
        // tl1.remove();

        // ②,没有强引用
        new ThreadLocal<>().set(22);
        System.gc();

        // ③,方法里的tl
        ThreadLocalTest2 test2 = new ThreadLocalTest2();
        test2.test1();
        System.gc();

        Thread thread = Thread.currentThread();
        System.out.println(thread);
		
    }

    void test1() {
        ThreadLocal<Integer> tl = new ThreadLocal<>();
        tl.set(33);
        // tl.remove();
    }
}

debug模式下,我们看下thread里ThreadLocalMap里的情况,如下图。
在这里插入图片描述
①因为存在强引用,所以对应的referent不为null。
②没有强引用,垃圾回收后,referent为null。
③ThreadLocal仅在test1()方法中有效,所以垃圾回收后,referent为null。

ThreadLocal源码解析

ThreadLocal类结构

public class ThreadLocal<T> {
	...
	static class ThreadLocalMap {
		
		static class Entry extends WeakReference<ThreadLocal<?>> {
			/** The value associated with this ThreadLocal. */
			Object value;

			Entry(ThreadLocal<?> k, Object v) {
				super(k);
				value = v;
			}
		}
		...	
	}
	...
}

从线程到ThreadLocal.set(value)的value的引用关系就是这样的

Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value

set(T value)

设置线程本地变量的内容。

public void set(T value) {
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
	if (map != null)
		map.set(this, value);
	else
		createMap(t, value);
}
void createMap(Thread t, T firstValue) {
	t.threadLocals = new ThreadLocalMap(this, firstValue);
}

get()

获取线程本地变量的value值

public T get() {
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
	if (map != null) {
		ThreadLocalMap.Entry e = map.getEntry(this);
		if (e != null) {
			@SuppressWarnings("unchecked")
			T result = (T)e.value;
			return result;
		}
	}
	return setInitialValue();
}
  • initialValue()
    设置 ThreadLocal 的初始化值,未 set(T value) 初次获取 Thread 对应的 Value 值时会调用,即被 setInitialValue 方法调用。需要重写该方法。
protected T initialValue() {
	return null;
}

remove()

移除线程本地变量。

public void remove() {
	ThreadLocalMap m = getMap(Thread.currentThread());
	if (m != null)
		m.remove(this);
}

ThreadLocal使用完后,一定要调用remove()!!!

不手动调用remove()方法,
1.容易发生内存泄漏。
2.线程池场景下后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。

几个问题

ThreadLocalMap的KEY为什么设计成弱引用

  • 如果设计成强引用,即使我们使用完ThreadLocal,只要线程没有结束,线程对应的ThreadLocal就不会被回收。这个时候当ThreadLocal太多的时候就会出现内存泄漏的问题。
  • 设计为弱引用后,只要没有了强引用,jvm垃圾回收时,便会将key(ThreadLocal)进行回收。
    降低内存泄漏的风险。

ThreadLocalMap的VALUE为什么不设计成弱引用

这里也用一个例子,来说明value为什么不是弱引用

public class TLLeakTest {
    public static void main(String[] args) {

        // ①,key和value均为WeakReference
        Map<WeakReference<Integer>, WeakReference<Integer>> map = new HashMap<>(8);
        WeakReference<Integer> key = new WeakReference<>(1);
        WeakReference<Integer> value = new WeakReference<>(777);
        map.put(key,value);
        System.gc();
        System.out.println("get1: " + map.get(key).get());


        // ②,key为WeakReference,value为HardReference
        Map<WeakReference<Integer>, Integer> map2 = new HashMap<>(8);
        WeakReference<Integer> key2 = new WeakReference<>(1);
        map2.put(key2, 999);
        System.gc();
        System.out.println("get2: "+ map2.get(key2));

    }
}
get1: null
get2: 999

以上结果,可以说明为什么value不能为弱引用。
如果value也为弱引用,那么得到的结果会为null。

ThreadLocal为什么内存泄露

来看例子。

  • 单线程模式
public class ThreadLocalLeakTest {
    public static void main(String[] args) throws InterruptedException {
        ThreadLocal<byte[]> local = new ThreadLocal<>();
        local.set(new byte[1024 * 1024 * 60]);

        Thread.sleep(100);

        local = null;

        while (true) {
            TimeUnit.SECONDS.sleep(1);
        }
    }
}

在这里插入图片描述
从上图的堆内存使用图可以看出,无论执行多少次GC,内存使用量总是在60M以上,即ThreadLocalMap中value的60M空间始终无法释放。

  • 线程池模式
    – ThreadLocal不remove()
public class ThreadLocalLeakTest2 {

    static ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES,
            new LinkedBlockingQueue<>());

    static ThreadLocal<byte[]> local = new ThreadLocal();

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 500; i++) {
            executor.execute(() -> {
                local.set(new byte[1024 * 1024 * 5]);
                System.out.println(Thread.currentThread().getName());
            });
        }

        local = null;
    }
}

在这里插入图片描述
从上图看出,这里无论执行多少次GC,堆内存依然会有25M左右的使用量,即5个线程 * 5M空间 = 25M。

使用完ThreadLocal,如果没有及时remove(),那么key因为是弱引用就会被回收,但value是强引用,value没被回收,导致value永远存在,造成内存泄漏。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值