ThreadLocal那些事

  每次在需要的时候都会去看源码,但是一直没有整理出来,这次借着这个机会整理一下。本文不会记录所有的源码内容分析,因为这种文章在网上随便都能搜到,不想复制粘贴。这里只想记录以下几个问题:

  • 在什么场景下需要使用ThreadLocal
  • ThreadLocal 与 Thread 的关联关系以及面试中经常被问到的点
  • 内存泄漏的问题

一、在什么场景下需要使用ThreadLocal

  在开发中我们肯定会遇到并发的场景,多个线程争抢更新同一个变量。这个时候我们可以选择采用锁的方式对变量进行控制,但是处理并发问题呢,其实还有另外一种方式。就是ThreadLocal。这里说是另外一种方式,就是说在处理并发问题上的处理思路与锁不同,ThreadLocal不是解决线程同步的问题,它是通过线程隔离来实现线程安全,每个线程保存一份自己的变量副本,这样就不会产生冲突 。

二、ThreadLocal 与 Thread

  ThreadLocal通过名字我们就可以理解,意思是线程本地变量,那就是每个线程拥有自己的,自然也就不会与其他线程产生通过并发的问题,也就没有数据一致性的问题了。那么JDK是怎么实现的呢?下面来记录几个比较重要的点。

  首先是ThreadLocal这个类,里面有一个内部类叫做ThreadLocalMap

static class ThreadLocalMap

这个类的内部结构比较简答,看过1.7版本及以前版本的HashMap的同学应该比较熟悉,就是一个简易的HashMap结构,唯一不同的是这个Entry是继承了WeakReference弱引用,这个在第三部分会详细说。

  这个ThreadLocal我们一般都是定义为成员变量,可是这个与线程有什么关系呢?怎么就成为线程的本地变量了呢?

我们这里简答看一下get方法:

    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();
    }

  首先是获取当前的线程,然后通过getMap方法来获取一个ThreadLocalMap的对象,这个就是上面我们说的ThreadLocal内置的一个Map的结构。

然后如果这个Map对象不为空,则找到里面的Entry,然后获取其中的value返回结果。

  这里面有一个在面试中会比较经常问答到点,就是ThreadLocal获取本地变量的时候,Key是什么值?通过上面的代码我们看到,是用的this引用,这个this就是这个ThreadLocal对象的引用。所以这里要特别注意,在获取ThreadLocal的Entry的时候,key是ThreadLocal的引用。

private Entry getEntry(ThreadLocal<?> key)

  接下来我们看一下getMap方法里面是怎么获取到的ThreadLocalMap对象的:

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

很简单,直接通过当前线程,返回threadLocals成员变量,那么有的同学就好奇了?这个在ThreadLocal中定义的内部类,怎么又成为Thread里面的一个成员变量了呢?

带着你的好奇,我们再来看一下get方法,上面我们是假设通过当前线程能够获取到ThreadLocalMap对象,那么如果获取不到呢?

就会执行``` setInitialValue ``` 方法,那么我们就看看这个方法里面做了什么:

    /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

首先通过initialValue方法获取初始值,然后获取当前线程,那么还是之前的流程,只不过在else分支中,这里如果获取不到ThreadLocalMap对象,那么就会调用createMap方法:

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

看到这里就很清晰了吧,这里创建了一个ThreadLocalMap对象,然后赋值当先线程的threadLocals成员变量。

到这里基本上ThreadLocal的核心设计逻辑基本就清楚了,所以下面我用自己比较通俗易懂的话概括总结一下:

  ThreadLocal在自己的内部类ThreadLocalMap中保存线程的本地变量,然后ThreadLocalMap对象又是以成员变量的角色存在与Thread里面,是不是觉得很绕,那为什么不直接把ThreadLocalMap对象直接定义到Thread里面呢?其实是这样,ThreadLocalMap存在Thread里面,可以方便的与线程关联起来,否则就需要在ThreadLocal中去维护所有线程的ThreadLocalMap对象,这样增加了查找和使用的开销。放到Thread里面在使用的时候则非常的方便。

  还有一个问题有的面试官也会问到,就是为什么要设计ThreadLocalMap,直接定义一个成员变量来存储线程本地变量值不就好了吗?我想上面我已经说清楚了,ThreadLocalMap是与线程绑定的,是Thread类的成员变量,可以这么理解,一个ThreadLocal对应一个Thread,但是一个Thread里面可以存在多个ThreadLocal啊,只是在第一次调用的时候会初始化ThreadLocalMap对象,后面就会通过ThreadLocal的引用,设置不同的成员变量,所以会设计ThreadLocalMap,然后通过Entry的方式来管理。

三、内存泄漏问题

  很多人因为听到内存泄漏而变得害怕,有些场景其实比较适合使用ThreadLocal,但是因为搞不懂为什么会出现内存泄漏,进而放弃使用ThreadLocal,其实我认为大可不必,下面我们一起来看看在什么情况下会产生内存泄漏?产生内存泄漏的原因又是什么?这样下次就不会因为害怕内存泄漏而不使用ThreadLocal了。

  首先解释一下上一小节中我们提到的,ThreadLocalMap里面的Entry数据结构是继承了WearReference弱引用,这里我们先简单回顾一些Java中的四种引用类型:

  • 强引用(Strong Reference):这个比较好理解,就是我们通过new,或者是反射创建的对象,只要我们不手动释放,就一直不会被垃圾回收调,这种对象就是强引用,大家都比较熟悉。
  • 软引用(Soft Reference)   :软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。
  • 弱引用(Weak Reference):弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收
  • 虚引用(Phantom Reference):虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。

 

这里为了便于理解,我手动画了个图,下面的内容我们结合图来进行说:

 

   具体产生泄漏的原因我已经在图片中写了一下。下面再详细的说明一下:

我们说ThreadLocalMap中的key是一个弱引用对象,那么这个引用对象被GC回收掉,导致存在于Entry中的value无法被回收,所以出现了内存泄漏。这个问题其实ThreadLocal已经做了兜底的处理来帮助我们避免这样的问题,就是在我们执行get,set,remove 的方法的时候,都会检查一遍当前的Entry ,   如果存在key为null的Entry会自动帮我们清理掉,这样就能够大概率的避免内存泄漏的出现。但是如果在线程中我们一直不去调用对应的方法,那内存泄漏就会一直存在,所以说最好的习惯就是在我们不需要使用ThreadLocal的时候,显示的调用remove方法。

 

一般的线程我们使用完成之后,随着线程生命周期的终结对应的ThreadLocal也就被回收了,所以不太容易出现内存方面的问题,但是如果我们使用的是线程池呢?对于corePollSize的线程是会被重复利用的,这个时候如果我们不显示的调用一下remove方法,带来的就不仅仅是内存的问题,而是更加严重的业务逻辑问题了,而且还特别的不好重现,增加了很大的排查难度。

所以如果在线程池中我们使用到了ThreadLocal,一定要重写一下afterExecute方法,在其中释放掉ThreadLocal信息,保证程序不会出现不可预知的错误。

posted @ 2021-04-16 12:14  SyrupzZ  阅读(54)  评论(0)    收藏  举报