面试常问的ThreadLocal是个啥?

80 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

基本功能

同一个ThreadLocal对象在不同线程中可以存储不同的值,线程之间的ThreadLocal值不会相互影响。基本使用如下所示:

@Test
    public void test() throws InterruptedException {
        ThreadLocal<Integer> intLocal = new ThreadLocal<>();
        Thread thread1 = new Thread(() -> {
            intLocal.set(1);
            System.out.println("thread1 set val 1");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("thread1 val: " + intLocal.get());
        });

        Thread thread2 = new Thread(() -> {
            intLocal.set(2);
            System.out.println("thread2 set val 2");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("thread2 val: " + intLocal.get());
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
    }
//    输出如下:
//    thread2 set val 2
//    thread1 set val 1
//    thread1 val: 1
//    thread2 val: 2

上例中thread1设置的intLocal和thread设置的intLocal为同一个对象,但是读取的值各不相同。

ThreadLocal实现方式

查看实现方式可以从ThreadLocal的源码入手 ThreadLocal类的set方法如下所示

public void set(T value) {
    Thread t = Thread.currentThread();  //拿到当前线程对象
    ThreadLocalMap map = getMap(t); //获取当前线程对象中存储的threadLocalMap,其中key在上例中的intLocal,而value存储的就是intLocal存的值。
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}

其中getMap源码如下所示

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

这样不同线程使用相同的ThreadLocal对象可以存储不同的值的原因就找到了,因为每个线程对象中都存储着自己的一份threadLocalMap,而ThreadLocal对象和ThreadLocal对象set的值是以key-value的形式存储到threadLocalMap中的,不同线程有各自的threadLocalMap,所以即使key相同,value也可以存储不同的值。

内存泄漏

因为点啥

Thread内存泄漏的原因是面试中问的频率比较高的,那我们来分析一下出现这种情况的原因

查看源码可以看到,ThreadLocalMap中实现key,value是使用内部类Entry来实现的,而Entry的构造方法源码如下所示:

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

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

可以看到key是存储到WeakReference中的,即弱引用。而弱引用在gc后会立即清理,这样就导致threadLocalMap中的key如果没有变量对其强引用的情况下,那么在GC时就会将其回收,而value是在Entry类对象中强引用指向的,所以value不会在gc时被回收。这样就导致value值一直存在,其实在线程中已经访问不到该value值了,导致内存泄漏。

举个栗子

模拟内存泄漏代码如下:

@AllArgsConstructor
public static class TestVal {
    @Getter
    String val;
}

@Test
public void gcTest() throws InterruptedException {
    for (int i = 1; i <= 100; i ++) {
        ThreadLocal<TestVal> intThread = new ThreadLocal<>();
        TestVal testVal = new TestVal("the obj " + i);
        intThread.set(testVal);
    }

    System.gc();    //触发gc
    Thread.sleep(3600000);
}

使用工具查看TestVal类对象数量: image.png TestVal类对象已经访问不到了,而堆中依然存在。就会发生内存泄漏的现象。

解决方式

对于这种内存泄漏的问题,只需要在不使用ThreadLocal时调用remove方法即可,示例代码如下所示:

@Test
public void gcTest2() throws InterruptedException {
    for (int i = 1; i <= 100; i ++) {
        ThreadLocal<TestVal> intThread = new ThreadLocal<>();
        TestVal testVal = new TestVal("the obj " + i);
        intThread.set(testVal);
        intThread.remove();
    }

    Thread.sleep(3600000);
}

上述代码在threadLocal失效前调用remove方法,执行后使用工具查看堆中TestVal类对象的数量如下 image.png 可以看到在gc后TestVal类对象都已经被回收了,这样就可以避免出现内存泄漏了。