likes
comments
collection
share

解读ThreadLocal

作者站长头像
站长
· 阅读数 21

解读ThreadLocal

简单使用


class ThreadLocalTest {

    companion object {

        /**
         * 定义一个ThreadLocal变量
         */
        private val threadLocal = object : ThreadLocal<Int>() {
            /**
             * 重写该方法,指定一个初始值
             */
            override fun initialValue(): Int {
                return 1
            }
        }

        @JvmStatic
        fun main(args: Array<String>) {
            /**
             * 开启子线程
             */
            thread(name = "threadLocal-1") {
                // 先获取旧值
                val oldValue = threadLocal.get()
                // 设置新值
                threadLocal.set(3)
                // 再获取新值
                val newValue = threadLocal.get()
                // 打印线程名称,旧值,新值
                println("thread name :${Thread.currentThread().name}| oldValue = $oldValue, newValue = $newValue")
            }

            /**
             * 开启子线程
             */
            thread(name = "threadLocal-2") {
                // 先获取旧值
                val oldValue = threadLocal.get()
                // 设置新值
                threadLocal.set(8)
                // 再获取新值
                val newValue = threadLocal.get()
                // 打印线程名称,旧值,新值
                println("thread name :${Thread.currentThread().name}| oldValue = $oldValue, newValue = $newValue")
            }
        }
    }
}

运行结果

解读ThreadLocal

实现原理

类图

解读ThreadLocal

从图中我们可以看到,在我们的测试类ThreadLocalTest中,开启了两个线程,thread-1thread-2,分别在两个线程中调用我们创建好的threadLocal变量的setget方法,先分别看下setget的源码

set方法


public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程的ThreadLocalMap成员变量
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 如果能获取到,则直接设置值
        map.set(this, value);
    else
        // 否则创建ThreadLocalMap对象再赋值
        createMap(t, value);
}

get方法

public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程的ThreadLocalMap成员变量
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 获取map对象中对应的Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            // 取出key对应的值并返回
            T result = (T)e.value;
            return result;
        }
    }
    // 否则创建ThreadLocalMap对象并返回initialValue()方法的值
    return setInitialValue();
}

getMap()

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

createMap()

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

set方法的源码中我们可以看到,首先会获取到调用set方法时程序所在的线程,然后再拿到当前线程对应的ThreadLocalMap成员变量,如果不为空,再调用ThreadLocalMap成员变量对应的set方法,把当前ThreadLocal对象作为key,保存到ThreadLocalMap对象中,为空,则创建再保存相应的值,ThreadLocalMap对象会持有一个Entry类型的数组,Entry是一个类似于Map结构的类,继承于WeakReference,get方法也有类似的逻辑,不做详细分析。

结论

不难发现,我们在调用ThreadLocalset或者get方法时,内部实际上调用的是ThreadLocalMapset或者get方法,我们可以理解为ThreadLocal只是对set或者get方法做了一层封装,而ThreadLocal本身作为一个壳供外部使用,而ThreadLocalMap是线程持有的成员变量,因此,我们在多线程的场景中使用ThreadLocal来保存变量,实际上就是不同的线程的成员变量ThreadLocalMap对变量做了一个副本,对变量的值的改变也只是改变了当前线程的变量的值,不会影响到其他线程的变量的值的改变,起到了一个线程隔离的作用。

关系图

解读ThreadLocal

简单解释下,ThreadLocalMapThread中的成员变量,ThreadLocalMap中持有Entry对象,Entry是一个类似于Map的数据结构的类,ThreadLocal作为key,Object作为值被保存下来,Entry是一个数组,目的是为了在同一个线程中能够保存多种数据类型的变量副本。

内存泄漏问题

为什么会导致内存泄漏

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统GC的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现keynullEntry,就没有办法访问这些key为nullEntryvalue如果当前线程再迟迟不结束的话,这些keynullEntryvalue就会一直存在一条强引用链:Thread -> ThreaLocalMap -> Entry -> value,永远无法回收,造成内存泄漏。 其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocalget(),set(),remove()的时候都会清除线程ThreadLocalMap里所有keynullvalue。 但是这些被动的预防措施并不能保证不会内存泄漏

为什么ThreadLocal设计成弱引用

在源码当中,ThreadLocal被设计成了弱引用,目的是降低OOM的可能性,不会因此避免OOM,我们都知道,弱引用在JVM中,当系统进行一次GC时,会回收掉被标记为弱引用的对象,ThreadLocalMap的生命周期是跟Thread的生命周期一致的,如果Thread的生命周期足够长,Thread会一直持有ThreadLocalMap对象:

  • 如果是强引用, ThreadLocalMap也会一直持有ThreadLocal,作为keyThreadLocal也就会随着ThreadLocalMap的生命周期一直存在,得不到释放,慢慢累积,key会越积越多,最终导致内存泄漏。
  • 如果是弱引用,GC时,当对ThreadLocal的强引用被回收时,系统会自动回收掉这部分ThreadLocal,并且当我们在下一次调用setget或者remove方法时,也会去清除掉这部分ThreadLcoal,从而降低对内存的消耗,但是ThreadLocalMap依然还持有对Entry的引用,而Entryvalue也是强引用,因此如果不手动释放,也会造成内存泄漏

如何避免

既然知道了造成内存泄漏的原因,那我们也就能够对症下药了,就是在每次使用完ThreadLocal的时候调用remove()方法清除掉value就行了。

转载自:https://juejin.cn/post/7232524757526446140
评论
请登录