ThreadLocal原理与实战
在Java的多线程并发执行过程中,为了保证多个线程对变量的安全访问,可以将变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立值,不会出现一个线程读取变量时被另一个线程修改的
现象。ThreadLocal类通常被翻译为“线程本地变量”类或者“线程局部变量”类。
ThreadLocal的基本使用
ThreadLocal位于JDK的java.lang核心包中。如果程序创建了一个ThreadLocal实例,那么在访问这个变量的值时,每个线程都会拥有一个独立的、自己的本地值。“线程本地变量”可以看成专属于线程的变量,不受其他线程干扰,保存着线程的专属数据。当线程结束后,每个线程所拥有的那个本地值会被释放。在多线程并发操作“线程本地变量”的时候,线程各自操作的是自己的本地值,从而规避了线程安全问题。ThreadLocal的英文字面意思为“本地线程”,实际上ThreadLocal代表的是线程的本地变量,可能将其命名为ThreadLocalVariable更加容易让人理解。
ThreadLocal如何做到为每个线程存有一份独立的本地值呢?
一个ThreadLocal实例可以形象地理解为一个Map(早期版本的ThreadLocal是这样设计的)。当工作线程Thread实例向本地变量保持某个值时,会以“Key-Value对”(即键-值对)的形式保存在ThreadLocal内部的Map中,其中Key为线程Thread实例,Value为待保存的值。当工作线程Thread实例从ThreadLocal本地变量取值时,会以Thread实例为Key,获取其绑定的Value。一个ThreadLocal实例内部结构的形象展示大致如图所示:
Java程序可以调用ThreadLocal的成员方法进行本地值的操作,具体的成员方法如表所示:
方法 | 描述 |
---|---|
ThreadLocal() |
创建一个新的ThreadLocal对象。 |
set(T value) |
为当前线程设置值。如果您调用该方法,它将将给定的值存储在当前线程的ThreadLocal对象中。 |
get() |
返回为当前线程存储的值。这是一个返回最后一次使用set() 或inheritedValue() 方法时存储的值。 |
initialValue() |
返回ThreadLocal的初始值。如果没有设置值,这个方法将返回与ThreadLocal对象相关联的初始值。 |
inheritedValue() |
返回ThreadLocal的继承值,如果没有设置值,该方法将返回从父线程继承的值或ThreadLocal对象的初始值。 |
remove() |
移除为当前线程存储的值。如果您调用该方法,它将清除与当前线程相关联的ThreadLocal对象中的值。 |
示例
1 | package pers.fulsun; |
运行以上示例,其结果如下:
1 | pool-1-thread-2初始的本地值::2@Foo{bar=10} |
通过输出的结果可以看出,在“线程本地变量”(LOCAL_FOO)中,每一个线程都绑定了一个独立的值(Foo对象),这些值对象是线程的私有财产,可以理解为线程的本地值,线程的每一次操作都是在自己的同一个本地值上进行的,从例子中线程本地值的index始终一致可以看出,每个线程操作的是同一个Foo对象。
1 | //获取“线程本地变量”中当前线程所绑定的值 |
ThreadLocal的使用场景
ThreadLocal是解决线程安全问题的一个较好的方案,它通过为每个线程提供一个独立的本地值去解决并发访问的冲突问题。在很多情况下,使用ThreadLocal比直接使用同步机制(如synchronized)解决线程安全问题更简单、更方便,且结果程序拥有更高的并发性。
ThreadLocal的使用场景大致可以分为以下两类:
线程隔离
: ThreadLocal的主要价值在于线程隔离,ThreadLocal中的数据只属于当前线程,其本地值对别的线程是不可见的,在多线程环境下,可以防止自己的变量被其他线程篡改。另外,由于各个线程之间的数据相互隔离,避免了同步加锁带来的性能损失,大大提升了并发性的性能。ThreadLocal在线程隔离的常用案例为:可以为每个线程绑定一个用户会话信息、数据库连接、HTTP请求等,这样一个线程所有调用到的处理函数都可以非常方便地访问这些资源。常见的ThreadLocal使用场景为数据库连接独享、Session数据管理等。在“线程隔离”场景中,使用ThreadLocal的典型案例为:可以为每个线程绑定一个数据库连接,使得这个数据库连接为线程所独享,从而避免数据库连接被混用而导致操作异常问题。跨函数传递数据
: 通常用于同一个线程内,跨类、跨方法传递数据时,如果不用ThreadLocal,那么相互之间的数据传递势必要靠返回值和参数,这样无形之中增加了这些类或者方法之间的耦合度。由于ThreadLocal的特性,同一线程在某些地方进行设置,在随后的任意地方都可以获取到。线程执行过程中所执行到的函数都能读写ThreadLocal变量的线程本地值,从而可以方便地实现跨函数的数据传递。使用ThreadLocal保存函数之间需要传递的数据,在需要的地方直接获取,也能避免通过参数传递数据带来的高耦合。在“跨函数传递数据”场景中使用ThreadLocal的典型案例为:可以为每个线程绑定一个Session(用户会话)信息,这样一个线程所有调用到的代码都可以非常方便地访问这个本地会话,而不需要通过参数传递。
使用ThreadLocal进行线程隔离
ThreadLocal在“线程隔离”应用场景的典型应用为“数据库连接独享”。下面的代码来自Hibernate,代码中通过ThreadLocal进行数据库连接(Session)的“线程本地化”存储,主要的代码如下:
1 | private static final ThreadLocal threadSession = new ThreadLocal(); |
Hibernate对数据库连接进行了封装,一个Session代表一个数据库连接。通过以上代码可以看到,在Hibernate的getSession()方法中,首先判断当前线程中有没有放进去Session,如果还没有,那么通过sessionFactory().openSession()来创建一个Session,再将Session设置到ThreadLocal变量中,这个Session相当于线程的私有变量,而不是所有线程共用的,显然其他线程中是取不到这个Session的。
使用ThreadLocal进行跨函数数据传递
ThreadLocal在“跨函数数据传递”场景的典型应用有很多:
- 用来传递请求过程中的用户ID。
- 用来传递请求过程中的用户会话(Session)。
- 用来传递HTTP的用户请求实例HttpRequest。
- 其他需要在函数之间频繁传递的数据。
通过ThreadLocal在函数之间传递用户信息、会话信息等,并且封装成了一个独立的SessionHolder类,具体的代码如下:
1 | public class SessionHolder { |
ThreadLocal内部结构演进
在早期的JDK版本中,每个ThreadLocal都创建一个Map,然后用线程作为Map的key,要存储的局部变量作为Map的value,这样就能达到各个线程的局部变量隔离的效果。
在JDK 8版本中,ThreadLocal的内部结构发生了演进,虽然还是使用了Map结构,但是Map结构的拥有者已经发生了变化, 每个Thread维护一个ThreadLocalMap,这个Map的key是ThreadLocal实例本身,value才是真正要存储的值object。
- 每个Thread线程内部都有一个Map(ThreadLocalMap)。
- Map里面存储ThreadLocal对象(key)和线程的变量副本(value)。
- Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
- 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
与早期版本的ThreadLocalMap实现相比,新版本的主要变化为:
- 拥有者发生了变化:新版本的ThreadLocalMap拥有者为Thread,早期版本的ThreadLocalMap拥有者为ThreadLocal。
- Key发生了变化:新版本的Key为ThreadLocal实例,早期版本的Key为Thread实例。
从上面变成JDK8的设计有什么好处?
- 每个Map存储的Entry数量变少,因为原来的Entry数量是由Thread决定,而现在是由ThreadLocal决定的。(真实开发中,Thread的数量远远大于ThreadLocal的数量。)
- 当Thread销毁的时候,ThreadLocalMap也会随之销毁,因为ThreadLocal是存放在Thread中的,随着Thread销毁而消失,能降低开销。
ThreadLocal源码分析
ThreadLocal源码提供的方法不多,主要有:set(T value)
方法、get()
方法、remove()
方法和initialValue()
方法。
set方法
1 | /** |
set()方法执行流程:
- 获得当前线程,然后获得当前线程的ThreadLocalMap成员,暂存于map变量。
- 如果map不为空,就将Value设置到map中,当前的ThreadLocal作为Key。
- 如果map为空,为该线程创建map,然后设置第一个“KeyValue对”,Key为当前的ThreadLocal实例,Value为set()方法的参数value值。
get方法
1 | /** |
get()方法执行流程:
1. 先尝试获得当前线程,然后获得当前线程的ThreadLocalMap成员,暂存于map变量。
2. 如果获得的map不为空,那么以当前ThreadLocal实例为Key尝试获得map中的Entry(条目)。
3. 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entrye,否则转到第4步。
4. 如果Entry为空,就通过调用initialValue初始化钩子函数获取ThreadLocal初始值,并设置在map中。如果map不存在,还会给当前线程创建新ThreadLocalMap成员,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map。
setInitialValue()方法的执行逻辑为:
1. 获取到value值(这里使用的initialValue默认返回null)。
2. 获取到当前线程,根据当前线程查询是否有对应的map。
3. 如果当前线程有对应的map,那么就更新值,否则的话就进行创建。
4. 最后instance of关键字用来判断this的类型是否属于TerminatingThreadLocal。如果this属于是TerminatingThreadLocal类型的,那么就调用register方法将this进行注册到TerminatingThreadLocal类中。
5. 最后的这段代码用于将终止类型的ThreadLocal实例注册到TerminatingThreadLocal类的静态列表中。这样,在线程退出时,终止类型的ThreadLocal实例会自动从ThreadLocalMap中移除,避免内存泄漏。
remove方法
1 | /** |
remove()方法执行流程:
1. 首先获取当前线程,并根据当前线程获取一个Map。
2. 如果获取的Map不为空,则移除当前ThreadLocal对象对应的Entry。
initialValue方法
1 | /** |
initialValue()方法的作用是返回该线程局部变量的初始值:如果没有调用set()直接调用get(),就会调用该方法,但是该方法只会被调用一次。默认情况下,initialValue()方法返回null,如果不想返回null,可以继承ThreadLocal以覆盖此方法。
真的需要继承ThreadLocal去重写initialValue()方法吗?其实没有必要。JDK已经为大家定义了一个ThreadLocal的内部SuppliedThreadLocal静态子类,并且提供了ThreadLocal.withInitial(…)静态工厂方法,方便大家在定义ThreadLocal实例时设置初始值回调函数。使用工厂方法构造ThreadLocal实例的代码如下:
1 | ThreadLocal<Foo> LOCAL_FOO = ThreadLocal.withInitial(() -> new Foo()); |
ThreadLocalMap源码分析
ThreadLocal的操作都是基于ThreadLocalMap展开的,而ThreadLocalMap是ThreadLocal的一个静态内部类,其实现了一套简单的Map结构(比HashMap简单)。
基本结构
ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现。
成员变量
跟HashMap类似, INITIAL_CAPACITY 代表这个Map的初始容量; table 是一个Entry类型的数组,用于存储数据; size 代表表中的存储数目; threshold 代表需要扩容时对应的size的阈值。
1 | /** |
存储结构 - Entry
在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。不过Entry中的key只能是ThreadLocal对象,这点在构造方法中已经限定死了。
另外,Entry继承WeakReference,也就是key(ThreadLocal)是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑。
1 | /* |
成员方法
ThreadLocal源码中的get()、set()、remove()方法都涉及ThreadLocalMap的方法调用,主要调用了ThreadLocalMap的如下几个方法:
方法名 | 描述 |
---|---|
find(int hash, ThreadLocal<?> firstKey) |
根据给定哈希值和第一个键查找对应的记录 |
createThreadLocalRegistry(Thread thread, int initialCapacity) |
创建一个新的 ThreadLocalMap 对象并初始化其容量 |
findSlotForNewValue(ThreadLocal<?> firstKey, int hash) |
找到可以放置新线程局部变量记录的位置 |
get(ThreadLocal<?> key) |
根据给定的 ThreadLocal 键获取对应的值 |
remove(ThreadLocal<?> key) |
从映射中移除指定的线程局部变量记录 |
set(ThreadLocal<?> key, Object value) |
将给定键和值添加到 ThreadLocalMap 中 |
withLock(Runnable runner) |
获取当前线程的锁,执行给定的操作后释放锁 |
对ThreadLocalMap的set(ThreadLocal<?>key,Object value)
方法的代码以注释的形式做一个简单的分析:
1 | private void set(ThreadLocal<?> key, Object value) { |
Entry的Key需要使用弱引用
什么是弱引用呢?仅有弱引用(Weak Reference)指向的对象只能生存到下一次垃圾回收之前。换句话说,当GC发生时,无论内存够不够,仅有弱引用所指向的对象都会被回收。而拥有强引用指向的对象则不会被直接回收。
什么是内存泄漏?不再用到的内存没有及时释放(归还给系统),就叫作内存泄漏。对于持续运行的服务进程必须及时释放内存,否则内存占用率越来越高,轻则影响系统性能,重则导致进程崩溃甚至系统崩溃。
为什么Entry需要使用弱引用对Key进行包装,而不是直接使用ThreadLocal实例作为Key呢?这个问题有点复杂,要分析清楚还有点难度。
这里从一个简单的例子入手,假设有一个方法funcA()创建了一个“线程本地变量”,具体如下:
1 | public void funcA() { |
线程tn调用funcA()方法新建了一个ThreadLocal实例,使用local局部变量指向这个实例,并且此local是强引用;在调用local.set(100)之后,线程tn的ThreadLocalMap成员内部会新建一个Entry实例,其Key以弱引用包装的方式指向ThreadLocal实例。
当线程tn执行完funcA()方法后,funcA()的方法栈帧将被销毁,强引用local的值也就没有了,但此时线程的ThreadLocalMap中对应的Entry的Key引用还指向ThreadLocal实例。如果Entry的Key引用是强引用,就会导致Key引用指向的ThreadLocal实例及其Value值都不能被GC回收,这将造成严重的内存泄漏问题, 因此在没有手动删除这个Entry以及CurrentThread依然运行的前提下,始终有强引用链CurrentThread Ref->CurrentThread->ThreadLocalMap->Entry ,Entry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。
同样假设在业务代码中使用完了ThreadLocal,ThreadLocal Ref被回收了。由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例,所以Threadlocal就可以顺利被GC回收,此时Entry中的key=null。
但是,在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 CurrentThread Ref->CurrentThread->ThreadLocalMap->Entry->value ,value不会被回收,而这块value永远不会被访问到了,导致value内存泄漏。也就是说,ThreadLocalMap中的key使用了弱引用,也有可能内存泄漏。
出现内存泄漏的真实原因
由于ThreadLocalMap中Entry的Key使用了弱引用,在下次GC发生时,就可以使那些没有被其他强引用指向、仅被Entry的Key所指向的ThreadLocal实例能被顺利回收。并且,在Entry的Key引用被回收之后,其Entry的Key值变为null。后续当ThreadLocal的get()、set()或remove()被调用时,ThreadLocalMap的内部代码会清除这些Key为null的Entry,从而完成相应的内存释放。
总结一下,使用ThreadLocal会发生内存泄漏的前提条件如下:
- 线程长时间运行而没有被销毁。线程池中的Thread实例很容易满足此条件。
- ThreadLocal引用被设置为null,且后续在同一Thread实例执行期间,没有发生对其他ThreadLocal实例的get()、set()或remove()操作。
- 第一点稍微复杂一点,由于ThreadLocalMap是Thread的一个属性,被当前线程所引用,所以它的生命周期跟Thread一样长。那么在使用完ThreadLocal的使用,如果当前Thread也随之执行结束,ThreadLocalMap自然也会被GC回收,从根源上避免了内存泄漏。
- 第二点很好理解,只要在使用完ThreadLocal,调用其remove方法删除对应的Entry,就能避免内存泄漏。
- 综上,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread-样长,如果没有手动删除对应key就会导致内存泄漏。
ThreadLocal编程规范推荐
编程规范有云:ThreadLocal实例作为ThreadLocalMap的Key,针对一个线程内的所有操作是共享的,所以建议设置static修饰符,以便被所有的对象共享。由于静态变量会在类第一次被使用时装载,只会分配一次存储空间,此类的所有实例都会共享这个存储空间,所以使用static修饰ThreadLocal就会节约内存空间。另外,为了确保ThreadLocal实例的唯一性,除了使用static修饰之外,还会使用
final进行加强修饰,以防止其在使用过程中发生动态变更。参考的实例如下:
1 | //推荐使用static final线程本地变量 |
凡事都有两面性,使用static、final修饰ThreadLocal实例也会带来副作用,使得Thread实例内部的ThreadLocalMap中Entry的Key在thread实例的生命期内将始终保持为非null,从而导致Key所在的
Entry不会被自动清空,这就会让Entry中的Value指向的对象一直存在强引用,于是Value指向的对象在线程生命期内不会被释放,最终导致内存泄漏。所以,在使用完static、final修饰的ThreadLocal实例之后,必须调用remove()来进行显式的释放操作。
如果使用线程池,可以定制线程池的afterExecute()方法(任务执行完成之后的钩子方法),在任务执行完成之后,调用ThreadLocal实例的remove()方法对其进行释放,从而使得其线程内部的Entry得以释放,参考的代码如下:
1 | // 线程本地变量,用于记录线程异步任务的开始执行时间 |
ThreadLocal综合使用案例
由于ThreadLocal使用不当会导致严重的内存泄漏问题,所以为了更好地避免内存泄漏问题的发生,我们使用ThreadLocal时遵守以下两个原则:
- 尽量使用private static final修饰ThreadLocal实例。
- 使用private与final修饰符主要是为了尽可能不让他人修改、变更ThreadLocal变量的引用
- 使用static修饰符主要是为了确保ThreadLocal实例的全局唯一。
- ThreadLocal使用完成之后务必调用remove()方法。这是简单、有效地避免ThreadLocal引发内存泄漏问题的方法。
下面用一个综合案例演示一下ThreadLocal的使用。此案例的功能为:记录执行过程中所调用的函数所需的执行时间(即执行耗时)。
比如在实际Web开发过程中,一次客户端请求往往会涉及DB、缓存、RPC等多个调用,一旦出现性能问题,就需要记录一下各个点耗时,从而判断性能的瓶颈所在。下面的代码定义了三个方法:serviceMethod()、daoMethod()和rpcMethod(),用于模拟实际的DB、RPC等调用,具体的代码如下:
1 | /** |
为了能灵活地记录各个执行埋点的耗时,这里定义了一个SpeedLog类。该类含有一个ThreadLocal类型的、初始值为一个Map<String,Long>实例的“线程本地变量”,名字叫作
TIME_RECORD_LOCAL。
如果要记录某个函数的调用耗时,就需要进行耗时埋点,具体的方法为logPoint(String point)。该方法会操作TIME_RECORD_LOCAL本地变量,在其中增加一次耗时记录:Key为耗时埋点的名称,值为当前时间和上一次记录时间的差值,也就是上一次埋点到本次埋点之间的调用耗时。
1 |
|
1 | public static void main(String[] args) { |