面试官:为什么threadLocal有自动清除机制还存在内存泄漏么?

介绍

ThreadLocal中设置的值仅属于当前线程,该值对其他线程而言是隔离的,所以在同一时间并发修改一个属性的值也不会互相影响。

使用

在使用ThreadLocal时,可以直接通过set(T value)get() 来设置threadLocal的值、获取threadLocal的值。

set方法

public void set(T value) {
    Thread t = Thread.currentThread(); // 获取当前线程
    ThreadLocalMap map = getMap(t); // 获取当前线程的ThreadLocalMap
    if (map != null) { // 如果map不是空
        map.set(this, value); // 设置值
    } else {
        createMap(t, value); // 创建并设置值
    }
}

// 获取线程的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals; 
}

// 对该ThreadLocal设置值
private void set(ThreadLocal<?> key, Object value) {

    // ThreadLocalMap内部的table数组
    Entry[] tab = table; 
    int len = tab.length; 
    // 根据threadLocal的hash和长度进行与运算,找到下标位置
    int i = key.threadLocalHashCode & (len-1);

    // 曾经该threadLocal有值,设置值并返回
    for (Entry e = tab[i];e != null; e = tab[i = nextIndex(i, len)]) {
        // 获取entry的引用
        ThreadLocal<?> k = e.get();
        // 引用等于当前threadLocal 则进行设置值
        if (k == key) {
            e.value = value;
            return;
        }
        // 当前引用为空,把key、value组装成entry放到i位置上,并清楚key为空的entry
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    
    // 组装entry
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 如果没有元素被清楚,并当前数组大小大于threshold则进行rehash;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}


其中 threshold = len * 2 / 3,它是通过setThreshold方法进行设置的。而每次rehash的时候都会调用resize方法,它会读取oldTable.length,把newLen设置为oldLen的两倍。这里有一个注意点int i = key.threadLocalHashCode & (len-1);下标是通过hash来确定的,会出现hash冲突,这里采用的是开放地址法来解决hash冲突,在下面的代码中有判断k==key,如果不相等则nextIndex(i, len)获取下一个下标来判断。

[需要看新机会的]

1、顺便吆喝一句,技术大厂,待遇给的还可以,就是偶尔有加班(放心,加班有加班费)

前、后端/测试,多地缺人,感兴趣的可以来试试~

2、或者是西安的C语言,国企芯片刚,统招本科及以上学历就成,接受测试岗,能熟练使用C语言,有嵌入式、芯片、驱动开发经验优先

上述就是整个set的过程,下面来看一下get

public T get() {
    Thread t = Thread.currentThread();
    // 获取当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // this为当前threadLocal,获取对应的entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            // 返回当前entry的值即可。
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 设置初始值并返回,初始值是null
    return setInitialValue();
}


private Entry getEntry(ThreadLocal<?> key) {
    // 查找下标
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        // 找到对应entry进行返回
        return e;
    else
        // 开始遍历entry数组,如果能找到key的entry就返回否则返回null
        return getEntryAfterMiss(key, i, e);
}


get方法要比set简单很多,只是根据key找对应entry,把entry的值返回即可。

结构

通过上述源码,可以总结出threadLocal的数据结构如下:

问题

根据上面的介绍,可以看出一些潜在的问题;例如在使用threadLocal时堆栈信息如下:

真的会内存泄漏?

当使用完threadLocal,threadLocal的对象引用就不存在了,而key对threadLocal是弱引用,gc后这段引用也不存在了。此时无法通过map.getEntry(this)找到对应的entry,而entry还一直存在Entry[]中,就有可能导致了内存溢出。这里我写了是有可能导致内存溢出,例如在set方法中有这样一行代码

if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();

该方法的具体代码如下:

private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}

当有新的threadlocal进行设置值时都会进行清除一下e.get() == null引用为空的Entry,而进入到这里的条件是(n >>>= 1) != 0,当长度为16(10000)会触发5次,挨着当前threadlocal的Entry的连续5个都没有引用为null的话,就不会继续往下移除了。所以如果频繁的调用set方法,它也会帮助清除一些之前key已经被gc掉的entry对象,但无论如何如果没有gc和调用set方法的话,这些entry对象会一直在内存中占用。

所以每次在使用完threadlocal时要调用一下remove方法,它会自动把entry移除。

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

除此之外在threadlocal时,尽量把它设置为pricate static变量,这样因为threadLocal的强引用一直存在,不会被垃圾回收掉这样就能保证任何时间都可以找到Entry,并对其进行remove

Entry的key设置为强引用可以么?

当ThreadLocal的引用在用户栈中已经移除了,并且没有调用remove方法;但是entry还有一个强引用指向threadLocal对象,e.get()永远都不会是空,此时entry对象就永远无法被回收掉了。

这样弱引用比强引用就多一层保障弱引用的 ThreadLocal 会被回收,对应value在下一次 ThreadLocaI 调用 get()/set()/remove() 中的任一方法的时候会被清除,从而避免内存泄漏。

示例:

public static void main(String[] args) {
    for (int i = 0 ; i < 100 ; i ++){
        ThreadLocal temp = new ThreadLocal();
        temp.set(i);
        temp = null;
    }
   // System.gc();
    ThreadLocal m = new ThreadLocal();
    m.set("value");
}

感兴趣的话,可以用上述示例跟着源码跑一遍源码的流程,当开启System.gc();时可以走到清理回收阶段。

子线程可以使用父线程的threadLocal中的值么?

不可以,如果想使用的话可以采用InheritableThreadLocal,它会在初始化子线程时进行设置子线程的threadlocal,也仅仅在初始化时有关联,后续子线程和父线程互相更改threadlocal都不会有任何影响。示例:

private static InheritableThreadLocal threadLocal = new InheritableThreadLocal();
@SneakyThrows
public static void main(String[] args) {
    threadLocal.set("1");
    Thread thread = new Thread(
            () -> {
                System.out.println("子线程获取threadLocal的值为:" + threadLocal.get());
                threadLocal.set("2");
            }
    );
    thread.start();
    Thread.sleep(200);
    System.out.println("父线程获取threadLocal的值为:" + threadLocal.get());
}

1、父线程先设置threadLocal的值为1;

2、开启一个子线程,获取threadLocal的值,得到结果为1;

3、子线程设置threadLocal为2,并且get一下,得到的结果为2;

4、睡眠200ms确保子线程命令都执行完成;

5、父线程获取threadLocal的值,得到的结果为1。

——转载自作者:想打游戏的程序猿

全部评论

相关推荐

评论
点赞
3
分享

创作者周榜

更多
牛客网
牛客企业服务