面试官:听说你精通并发编程,来说说你对ThreadLocal的理解

1,290 阅读10分钟

ThreadLocal 简介

ThreadLocal 是一个解决多线程并发问题的工具类,ThreadLocal有的人可能理解为本地线程,这个并不是正确的理解。ThreadLocal并不是一个线程,应该把它理解为一个线程本地变量

它底层的实现原理是通过为每一个线程提供一个共享变量的副本,每个线程的操作只是对自己线程内部的变量副本进行操作

这里提到线程操作只对自己的内部变量副本进行操作很多人第一反应就会想到线程的工作内存以及JMM的知识点,会误以为ThreadLocal是解决并发中共享数据的访问问题

但是,ThreadLocal并不是为了解决多线程共享变量的问题,它只是为每个线程提供一个变量副本,这个变量副本对于其它线程来说是不可见的

解决并发中共享数据的访问问题和为线程提供一个只能在自己线程内部操作的线程本地变量还是有本质区别的。

并且,ThreadLocal是一直伴随着当前线程的整个生命周期,随着线程的消亡而消亡,他这个变量副本不需要与其它线程进行共享。

在jdk1.2的时候就提供了ThreadLocal类,在jdk逐渐升级的过程也对ThreadLocal进行了升级优化,在jdk1.5的时候ThreadLocal开始支持泛型

ThreadLocal 源码实现

从上面的简介出发,我们说到ThreadLocal 本质上是一个线程本地变量,对于一个变量来说,要操作它就要对外提供get、set、remove、init等方法。

对于ThreadLocal的源码底层的实现是比较简单的,主要就是几个的方法源码,而且实现的逻辑也很少,所以强烈的推荐阅读,对于初级开发人员来说难度也不大。

我们来看看ThreadLoca的底层源码,在ThreadLoca中,主要的方法包含以下几个:

  1. get:获取当前线程本地变量的值。
  2. setInitialValue:第一次获取当前线程本地变量的值的时候,进行值得初始化,若不重写setInitialValue方法,初始值为null。
  3. set:设置当前线程变量的值。
  4. remove:删除当前线程本地变量的值。

接下来我们对这几个方法一层一层进行剖析,先来看看get方法得源码实现:

get的方法的源码非常的简单,从中可以看出主要分为三步

  1. getMap获取map的值。
  2. 从map中获取value值。
  3. 若是上面两步获取值为null,就进行初始化。

get源码中有一个getMap的方法实现,我们来看看getMap方法的源码实现:

这个方法实现非常的简单t.threadLocals来获取ThreadLocalMap对象,从这一点就可以证实ThreadLocalMap是属于当前Thread的,如果Thread对象不同,那么获取的对象肯定是不一样的

倘若获取的值为null,就会调用setInitialValue方法进行初始化,来看看他的初始化的源码:

从初始化的源码中可以看出value直接就是为null,初始化map肯定是不存在的,所以调用createMap的方法进行创建map,来看看创建map的源码

直接就是调用new ThreadLocalMap(this,firstValue)进行初始化,并赋值给当前线程的threadLocals引用,在每个线程中都有一个空的threadLocals引用,源码如下:

在这里插入图片描述
在这里插入图片描述

所以这里又再一次的证明了,ThreadLocal是属于当前线程的,也就是ThreadLocal中的ThreadLocalMap属于当前线程,对于其它线程是不可见的

从上面的源码分析逻辑中,可以深入的了解到ThreadLocal的get方法处理逻辑,我画了一个get方法的处理流程图,如下图所示:

分析完get方法的源码来看看set方法的源码,可以说get方法的源码在ThreadLocal已经算是比较复杂的了,这回大家心里是不是在想,这也太简单了吧。

ThreadLocal就是那么简单,下面的几个方法的源码的分析就会越来越简单,我们来看看set方法的源码:

这几个方法都很熟悉了呀,getMap的实现就是返回t.threadLocals,然后就是判断map是否为空,不为空就map.set(this,map),为空就调用createMap创建map值。

set方法非常的简单。所以在第一次进行调用get和set方法的时候,都会进行初始化线程的ThreadLocalMap,但是如果先调用get方法,初始化的ThreadLocalMap的value值是null

看完get和set方法的源码,最后就来看看remove方法的源码:

在移除操作中是调用了ThreadLocalMap的remove方法,而ThreadLocalMap的remove方法中会根据key的threadLocalHashCode值和数组的长度先计算下标值。

然后循环的从i下标开始循环的获取key数组中的Entry值e,若是Entry对象的key与要移除的key相等,才会把它移除掉。

Entry对象是ThreadLocalMap中的一个static内部类,ThreadLocalMap内部是使用Entry来实现key-value来实现存储的,并且Entry继承弱引用:

这里的Entry类似与我们的HashMap结构,都是使用key-value形式进行存储,但是他们还是有区别的。

Map集合中的hash类结构解决hash冲突采用的是拉链法,而在Entry解决hash冲突采用的是开放定址法

什么是开放定址法呢? 当发生冲突的时候,第一个key占据了一个位置,第二个key第一个key的位置向下查找,找到下面的一个空位插入, 如果没有继续查找空位,直到找到为止并进行插入。

这个可以从Entry的源码中可以看出,我们来看看Entry里面的set方法的源码:

从Entry的源码中就可以看出,当计算出下标i的值,并获得对应数组i位置的值若是key不相等,从该位置一直循环获取key值并于当前的key值进行比较,直到相等为止。

ThreadLocal 存在问题

在set方法中,有一个重要的作用就是防止内存泄漏,在set中分别调用了replaceStaleEntry()cleanSomeSlots()方法,两者的作用分别如下:

  1. replaceStaleEntry:当判断到key为null,但是存在值说明,之前的key(ThreadLocal对象)被清除掉,但是内存一直占用着,就用新的元素替换掉旧的元素。
  2. cleanSomeSlots:则清除掉原来key == null的Entry。

在说ThreadLocal内存泄漏的解决的方案之前,先来说说造成内存泄漏的原因,这里我画了一张图:

在Thread有一个ThreadLocalMap的引用,而ThreadLocalMap的key在源码中可以看出是ThreadLocal的对象。

但是ThreadLocal是一个弱引用,在垃圾回收的时候会被回收掉。什么是弱引用呢? 弱引用就是被弱引用关联的对象只能存活到下一次垃圾收集发生之前。当垃圾收集器工作时,弱引用关联的对象都会被回收。

但是ThreadLocalMap的生命周期和Thread是一样的,它不会被回收掉,所以就会存在有引用指向Entry对象。

若是线程一直不结束,就会一直存在一条引用链:ThreadLocalRef->Thread->ThreadLocal->ThreadLocalMap->Entry->value导致ThreadLocalMap中的key没有了,但是value还存在,这就造成了内存泄漏

此时若是key值的引用ThreadLocal对象被回收掉,就无法通过key获取对象,就会存在内存中又无法使用。

在ThreadLocal里面为了防止内存泄漏的情况,在新增、移除、获取的时候,都会去查出key==null的entry对象。

所以为了避免内存的泄漏,每次使用完ThreadLocal时候都养成调用remove方法来擦出数据的习惯,即时的清理出干净的内存。

ThreadLocal 应用场景

看上面的介绍我们已经详细的了解了ThreadLocal的作用和实现的原理,那么它的应用场景都有哪些呢?

这里主要列举了两方面,一个就是数据库连接,Session会话管理;另一个就是在Spring的MVC三层架构方面,用来获取request 这个参数。

我们先来说一说数据库连接,Session会话管理这一方面的应用,有深入了解过数据库的人可能知道,在使用我们Java代码操作数据库,首先要建立连接。

但是这里就有一个问题,一个人使用完数据库后,要经历连接->断开这样的一个过程,加入大量的用户过来,那么这对数据库来说是一笔巨大的开销。

我们这里就可以使用ThreadLocal,这样ThreadLocal就会在每个线程中都都创建一个副本,并且这个副本在线程的任何地方都可以使用。这样就不用频繁的断开又建立连接。

另一方面对request这个参数的管理,可能在多个很多方法中都会用到request这个参数。我们可以把这个参数都放到ThreadLocal中,随时用就随时取。

并且对于多线程中,request是不需要被共享的,只要属于当前线程即可,所以说它并不是解决多线程变量的共享问题,这个和Synchronized 还是又明显区别的。

说完ThreadLocal的场景的分析,下面来说一说常见的ThreadLocal的面试问题,这里的面试问题的答案仅是个人的理解和思考,大家可以参考,若是又更加深入的思考可以在留言区留言。

ThreadLocal 面试问题

问点一:为什么使用线程的id来作为ThreadLocalMap的key值呢?

这个很容易理解,不使用Thread的id来作为ThreadLocalMap的key主要是为了解决存储多value的情况。

一个线程就对应一个ThreadId,假如有多个value需要存储,就没办法存储,而已ThreadLocal作为key,每个ThreadLocal的唯一性使用threadLocalHashCode来区分,就可以对应多个value进行存储。

问点二:线程之间如何传递ThreadLocal对象呢?

在ThreadLocal的子类InheritableThreadLocal中有了对应的实现,InheritableThreadLocal重写了ThreadLocal 中的三个相关的方法。

通过这个实现,可以实现子父线程之间的数据传递,在子线程中能够使用父线程的ThreadLocal本地变量。

问点三:ThreadLocal是如何做到变量在线程之间是互不干扰的?

在ThreadLocal中内部维护了一个ThreadLocalMap用于储存数据,而每一个线程中都会有一个ThreadLocalMap的引用,在第一次使用ThreadLocal的时候就会创建map。

线程不同必然t.threadLocals是不同的,每个线程在第一次使用的时候,都会进行初始化map,因此在不同的线程中也就互不干扰,独立于线程而存在。

问点四:ThreadLocal中造成内存泄漏的原因?解决方案是什么?

因为ThreadLocal被包装成弱引用,在ThreadLocal对象被回收时,还会存在ThreadLocalRef->Thread->ThreadLocal->ThreadLocalMap->Entry->value引用链。

这样就导致这块内存无法被回收,且又不能被使用,造成了内存泄漏,为了能够有效的避免内存泄漏,就要养成使用完后调用remove方法擦出数据习惯。