ThreadLocal从入门到精通

900 阅读11分钟

1.ThreadLocal是什么

ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。

——《Java并发编程艺术》

首先,我们站在人类认知事物的角度,按照下面这张图的思路出发:

image-20201125171509927
  • 首先看到ThreadLocal,可以拆成Thread+Local
  • Thread—线程;local—本地的,局域的。
  • 拼在一起就是线程局域的。线程私有的。

OK,如果你想到了这一点,其实你已经知道了ThreadLocal的最大特点——线程私有。这种理解是对的,ThreadLocal类顾名思义可以理解为线程本地变量。也就是说如果定义了一个ThreadLocal,每个线程往这个ThreadLocal中读写是线程隔离,互相之间不会影响的。它提供了一种将可变数据通过每个线程有自己的独立副本从而实现线程封闭的机制。

2.ThreadLocal怎么用

下面是一段很简单的代码,展示了ThreadLocal的基本用法:

image-20201127180205888

  • CAR_HOUSEKEEPER是一个ThreadLocal对象,以下简称车辆管家
  • 主线程中创建了两个线程:富商A线程和富商B线程,以下简称A和B。
  • A把自己买的“兰博基尼”交给车辆管家
  • B也把自己买的“兰博基尼”交给车辆管家
  • A需要用车的时候,车辆管家给他的是A的兰博基尼
  • B需要用车的时候,车辆管家给他的是B的兰博基尼
  • A把车卖了,就拿不到车了

ThreadLocal三个重要方法:

  • set:存数据(叫管家停车)
  • get:取数据(叫管家取车)
  • remove:删除数据(叫管家卖车)

3.ThreadLocal原理

错误的认识

看完ThreadLocal的使用,聪明的你肯定会在脑海里想,怎么能做到这样呢?于是乎,在你的脑海里出来了下面这一张错误的结构图:

image-20201127164857344

那么为什么不这样设计呢?或者说这样设计有什么缺点呢?

如果按照上面这种设计的话,我们打个比方:

你是一位成功的商业人士,非常有钱,家里有一栋一万平的别墅。于是你雇了两个管家。一个管家帮你管理跑车,一个管家帮你管理自行车,这是大前提。

  • 今天你买了一辆兰博基尼,你把钥匙交给第一个管家,第一个管家把车停到他自己家的停车场去了;
  • 第二天你又买了一辆自行车,你把自行车交给第二个管家,第二个管家把车骑到他自己家的停车场去了;
  • 当你需要用兰博基尼的时候,第一个管家去他家里帮你把车开来。
  • 当你需要骑自行车的时候,第二个管家去他家里帮你把车骑来。

把上面的例子抽象一下,Thread对应你,ThreadLocal对应管家,ThreadLocal里面的Map对应管家的车库。你是不是会发现这样的模式非常不合理?我买的兰博基尼明明是我用的,为什么不停在我的家里?管家本来只需要停车,现在却要在自己家里建一个车库。是不是莫名其妙?

image-20201127133231878

实际的设计

还是上面那个例子,你觉得非常不合理,你决定这样做:

  • 今天你买了一辆兰博基尼,你把钥匙交给第一个管家,管家把车停到你家车库的某个地方;
  • 没有车库的话,管家帮你招人建一个车库,然后把车停进去。
  • 第二天你又买了一辆自行车,你把自行车交给第二个管家,管家把车放到到你家车库的某个地方;
  • 而你只需要关注谈商业合作,处理一些重要的事情。
  • 当你需要用兰博基尼的时候,只需要叫第一个管家把车从你的车库开来就行。
  • 当你需要骑自行车的时候,只需要叫第二个管家把自行车从你家车库骑过来就行。

显然,这样就变得合理多了。

Josh Bloch和Doug Lea两位大师受到了你的启发(开个玩笑),就设计出了我们现在用到的ThreadLocal。

Thread对应你,ThreadLocal对应管家,你家车库对应Thread中的一个Map。不同种类型的车对应不同的类,但是这些不同类型的车都属于车(Object)。

我们在上面的例子上,加一个条件——允许管家可以打多份工。可以得到了下面简图:

image-20201127161212065

我们抽象一下,结合代码,看看调用set()方法的时候,到底干了什么呢?我们看一下动态图:

3

从上面的动图,我们可以很清楚的看到,在调用ThreadLocal的set()方法时,数据是如何存储的。当然,上面的图不是内存分布图,只是一个简图,不够严谨,只是为了让你了解基本的原理。

可以总结成一句话:

每个线程内部都有个map,调用ThreadLocal.set(object)方法时,把这个ThreadLocal对象作为key,object作为值存到了这个内部map里面。

我们用一张图翻译一下这句话,得到的就是ThreadLocal和Thread之间的关系:

image-20201127190456886

源码分析

以下的部分会非常非常干,需要有点GC的基础,不过,只需要一点点就行。

ThreadLocal结构

ThreadLocal的API非常的简单,ThreadLocal对外就提供了五个方法:

  • ThreadLocal():构造方法
  • withInitial():静态方法,通过这个方法创建ThreadLocal可以重写initialValue方法(返回当前线程的这个线程私有变量的“初始值”),自己设置默认值
  • get():获得此线程私有变量的当前线程副本中的值
  • set(T):设置此线程私有变量的当前线程副本中的值
  • remove():删除此线程私有变量的当前线程副本中的值

image-20201126202901621

public class ThreadLocal<T> { 
  
    /**
     * 创建一个线程局部变量。
     * 变量的初始值是通过调用Supplier函数的get方法来确定的。
     * SuppliedThreadLocal是ThreadLocal的一个扩展,它是一个函数
     */
    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }
  
    /**
     * 设置此线程私有变量的当前线程副本中的值
     */
    public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的threadLocals(ThreadLocalMap)
        ThreadLocalMap map = getMap(t);
        //如果threadLocals已经被初始化了,把值放进去;如果不存在,先初始化再把值放进去
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    /**
     * 获得此线程私有变量的当前线程副本中的值
     */
    public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的threadLocals(ThreadLocalMap)
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //从threadLocals获取节点(Entry)
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                T result = (T)e.value;
                return result;
            }
        }
        //如果map为空,返回的初始值,如果没有重写initialValue方法,返回的是null
        return setInitialValue();
    }
  
    /**
     * 删除此线程私有变量的当前线程副本中的值
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
}

上面一段代码,简化了一些细节,把ThreadLocal对外提供的重要的四个方法的源码列了出来,你只需要知道Thread.currentThread()this关键字是干什么的,就能非常容易的理解了。

ThreadLocalMap结构

ThreadLocalMap是ThreadLocal的一个静态内部类。里面的核心是一个Entry数组,Entry继承了WeakReference,在创建Entry的时候,将ThreadLocal对象设置成了弱引用。

注意,ThreadLocalMap虽然是ThreadLocal里面的一个静态内部类,但是它的实例是放在Thread里面的,这个地方一定要分清楚,前面没有提及就是怕大家混淆。

 static class ThreadLocalMap {
     /**
     * 初始容量,默认为16,必须为2的幂
     */
    private static final int INITIAL_CAPACITY = 16;

    /**
     * 表里entry的个数
     */
    private int size = 0;
   
    /**
     * Entry表,大小必须为2的幂
     */
    private Entry[] table;
   
    /**
     * Entry继承了WeakReference
     * Entry的构造方法中调用了super(k),将ThreadLocal对象设置成了弱引用
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
            /** value就是和ThreadLocal绑定的,为实际放入的值 */
            Object value;

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

知道了ThreadLocalMap的机构,我们用一张时序图表示调用ThreadLocal的get()方法的流程(其中的create表示调用默认的构造方法):

1
什么是弱引用

Java存在四种引用关系(如下表)。如果一个对象存在一个弱引用,那么他会在下一次GC的时候被回收,至于为什么,是怎么做到的,本文不做过多的分析。我们只需要知道它的一个最大特点——下一次GC被回收。

引用类型回收时间应用场景
强引用一直存在一般对象
软引用内存不足会被回收缓存
弱引用下一次GC被回收缓存,ThreadLocal
虚引用虚引用必须要和引用队列一起使用,他的get方法永远返回nullJVM堆外内存管理

弱引用的例子:

image-20201128160948983
为什么要用弱引用

我们假设用强引用,会出现什么问题呢?我们看一下这张动态图:

4

从上面的图我们可以看出,如果不用弱引用而用强引用的话:

当ThreadLocal对象不用的时候,将他的引用设置成null,引用所指的堆中的ThreadLocal是没有办法被回收的,永远存在一条ThreadRef->Thread->ThreadLocalMap->Entry->ThreadLocal的强引用链。导致这一部分的内存无法被回收,造成内存泄漏。

而弱引用是Java中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次GC。当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利。

InheritableThreadLocal

InheritableThreadLocal继承了ThreadLocal,在ThreadLocal的基础上做了一点扩展,提供了一种父子线程之间的数据共享机制,在这里不多做介绍,有兴趣的可以去了解一下。

4.ThreadLocal内存泄漏

所谓的内存泄漏,就是程序申请的内存无法被JVM回收。

什么情况下会内存泄漏

前面已经说了ThreadLocal怎么用弱引用避免了内存泄漏。但是,如果使用不当,还是会出现内存泄漏的。注意,这是两个问题。那么,怎么样使用会造成内存泄漏呢?

我们把JVM的最大堆设置成100MB,运行下面的代码,用JProfiler查看内存使用情况,发现内存使用不断增大,直到抛出java.lang.OutOfMemoryError: Java heap space也就是OOM异常。

image-20201128231544154

image-20201128230720157

image-20201128230630097

我们分析一下代码:

  • 创建了一个核心线程数和最大线程数为5的线程池,这个保证了线程池里面随时都有5个线程在运行
  • 模拟50个任务,每隔2秒往线程池里面加一个任务
  • 任务:创建一个User对象user,给user的threadLocal赋值一个新创建的ThreadLocal对象,往这个ThreadLocal对象里面加一个5MB的Memory对象。

我们画出内存分配图:

5

图中模拟了内存的分布和GC之后部分内存的回收,我们可以清楚的看到:

  • ThreadLocal对象存在两个引用,实现代表强引用,虚线代表弱引用。
  • 强引用因为引用对象被回收了引用不存在了
  • 虚引用是不能阻止GC的回收的
  • 最终Entry的key最终指向的是null,而value指向的还是占用5MB内存空间的Memory对象。
  • 这个时候,存在一条ThreadRef->Thread->ThreadLocalMap->Entry->Memory的强引用链,导致Memory无法被回收,造成内存泄漏,最终导致OOM。

通过上面的内存分配图,我们不能得出:

如果线程运行完任务就结束了,ThreadRef->Thread->ThreadLocalMap->Entry->Memory这条引用链就不存在了,就不存在内存泄漏的问题了。

但是现在的Java应用,为了节省开销,大部分都会采用线程池的模式。为了不造成内存泄漏,最简单有效的方法是使用后调用remove()方法将其移除。

5.ThreadLocal的应用场景

  • Spring事务
  • APM的traceId
  • Session管理
  • JDK7使用SimpleDateFormat
  • 数据库的连接池
  • ......

总之,有以下两个特点的地方,都可以用到ThreadLocal:

  1. 方法调用链路很长,很多地方都需要用到这个参数,避免不必要的参数传递
  2. 要求线程间数据隔离

6.总结

本博文重点介绍了ThreadLocal中ThreadLocalMap的大致实现原理以及ThreadLocal内存泄露的问题。旨在让大家对ThreadLocal能有一些新的理解。

文章省略了一些细节的问题,对于ThreadLocalMap的算法实现和它用线性探测法来解决散列冲突没有做介绍。因为我认为关于散列冲突的问题,是需要专门写一篇文章来介绍的。其中将数组当做环来用的思想也非常经典。说到环,又不得不提Disruptor和一致性hash,这一些经典的设计,希望以后能慢慢的给大家分享。

作为Josh Bloch和Doug Lea两位大师之作,ThreadLocal本身实现的算法与技巧还是很优雅的,非常值得一看。这一部分的代码是我在JDK中最喜欢的一段代码,每次看到,都是称赞不已。

写在最后

文笔虽烂,但喜欢分享,有时分享技术,有时分享生活。

我是CoderWang,一个Java程序员。

我们下期再见!

如果可以,点赞、加关注,谢谢你!

更多精彩微信公众号搜索“CoderW”,我们一起进步!