学习笔记
🗒️ThreadLocal内容详解
00 分钟
2023-11-3
2024-6-29
type
status
date
summary
slug
tags
category
password
URL
icon
 

Java引用类型介绍

Java中有四种不同类型的引用类型,它们在垃圾回收的行为上有所不同:

强引用

这是最常见的引用类型,当我们创建一个对象并将其赋值给一个引用变量时,默认情况下就是强引用。
只要某个对象有强引用指向它,垃圾收集器就不会回收这个对象。例如:
 

软引用

通过SoftReference类实现,软引用是为了解决内存敏感的缓存问题。垃圾收集器在内存不足时会回收这些对象,如果内存充足,则不会回收。
使用SoftReference可以实现一个内存敏感的高速缓存。例如:
 

弱引用

通过WeakReference类实现,比软引用生命周期更短。无论内存是否足够,只要发生垃圾回收,弱引用指向的对象就会被回收。
 

虚引用

通过PhantomReference类实现,是所有引用类型中最弱的一个。一个持有虚引用的对象,和没有任何引用的对象一样,在任何时候都可能被垃圾收集器回收。
虚引用主要用于跟踪对象被垃圾回收的活动,与ReferenceQueue配合使用。
 

Java内存区域介绍

JDK1.8以后的内存分布图如下所示,原图来自Java内存区域详解(重点) | JavaGuide(Java面试 + 学习指南)
 
notion image
 
从图中可以看出,堆(包含字符串常量池)是线程共享的,而虚拟机栈、本地方法栈以及程序计数器是线程私有的。
:堆是Java虚拟机所管理的内存中最大的一部分,Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。字符串常量池是JVM为了提升性能和减少内存消耗针对字符串类型专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
程序计数器:程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令。每个程序计数器是线程私有的,线程之间互不影响。
虚拟机栈:虚拟机栈与方法调用有直接关系,方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。每个栈帧都拥有:局部变量表、操作数栈、动态链接、方法返回地址。
本地方法栈:和虚拟机栈的功能类似,区别是:虚拟机栈为虚拟机执行Java方法,而本地方法栈则为虚拟机使用到的Native方法服务。在HotSpot虚拟机中和Java虚拟机合二为一。
 

ThreadLocal简介

ThreadLocal是Java中的一个类,不同于一般类,它是一个线程的局部变量。在JDK1.8中的官方介绍如下所示。
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
翻译成中文就是:该类提供局部线程变量,这个变量与其他常见变量的区别是,访问这个变量的每个线程都有其自己的、独立初始化的变量副本。ThreadLocal实例通常是类中希望将状态与线程关联起来的私有静态字段(如用户ID或者事务ID)。
 

ThreadLocal类源码解析

 
ThreadLocal类的源代码如下,我们可以先看几个重要方法get()和set()。
get方法的具体实现如下:
 
我们先看一下getMap方法。
getMap方法直接返回线程Thread中的threadLocals变量,点进Thread类中看一下这个变量的类型,也是ThreadLocalMap类型。那我们再看一下这个类的具体构成,这个类属于ThreadLocal类的静态内部类:
从代码中我们可以看出,ThreadLocalMap这个类中的重要变量是这个Entry数组:table。其中Entry类是一个继承了WeakReference的类,其构造器接收Threadlocal实例和一个对象value。当我们向ThreadLocalMap中添加元素时,会先通过哈希计算获取到这个元素应该存入table中的位置,如果存在哈希冲突,则向后顺延一位,如果后面这个位置上没有元素,则直接存储到这个位置,否则继续后移直到找到空的位置。
取数据的时候,也是通过哈希计算获取到当前ThreadLocal对象在table中的位置,判定该位置中存储的是否是当前ThreadLocal对象,如果是的话,直接返回;否则按照一定规则继续查找。
 
看完了这个,我们继续看下向ThreadLocal中插入数据的逻辑。
 
插入的逻辑也比较简单,先判断当前线程的ThreadLocalMap是否存在,不存在则创建一个,存在的话则调用ThreadLocalMap的set方法插入数据。set方法我们上面讲过了,因此这里不再赘述。
 

ThreadLocal家族变种

InheritableThreadLocal

该变种实现了在父子线程之间传递ThreadLocal数据的问题,这个类会在创建子线程时将父线程的变量副本复制到子线程中,实现子线程继承父线程的变量副本。注意,这里的继承的时变量副本,而不是变量本身或者变量本身的引用,如果创建完子线程后,父线程中的ThreadLocal数据发生变化,子线程是感知不到的。
 

TransmittableThreadLocal

该变种是为了解决线程池中父子线程数据传递的问题。上面的InheritableThreadLocal类主要是解决父子线程的数据传递问题,但是没办法解决线程池的数据传递问题。因为线程池中的线程存在线程复用的问题,所以父线程传递给子线程的数据并非是子线程真正需要的。
 

FastThreadLocal

 
我们上面提到 ThreadLocalMap 是通过线性探测的开放定址法解决 Hash 冲突的问题,当 Hash 冲突严重时该方案会出现性能问题。为此 Netty 进行了优化,也是运用空间换时间的思想,通过用一个属性保存 map 的下标位置,用下标直接找到对应的位置,来应对大数据量和大并发的场景。但是 FashThreadLocal 在使用时有一个需要特别注意的地方,就是必须与 Netty 提供的 FastThreadLocalThread 结合使用才能发挥作用。
 

ThreadLocal的应用场景 && 注意事项

非线程安全转化为线程安全

在 JDK 对外提供的各种类中,并不是所有类都是线程安全的,而 ThreadLocal 一个非常重要的作用就是能将这些非线程安全的类以线程安全的方式使用。比如 SimpleDateFormat 本不是线程安全的,通过 ThreadLocal 的改造却可以当成线程安全的类来使用。

解决跨层传递变量的问题

在复杂的系统中,可能需要在不同的层之间传递变量,例如在 Controller 层中获取用户信息,然后在 Service 层中使用用户信息。用 ThreadLocal 可以在当前线程中存储变量值,避免跨层传递变量的问题。
 

注意事项

  • 内存泄漏问题
  • 脏数据问题
  • 性能问题
  • 避免降低代码的可读性
  • ThreadLocal不适合存放大量数据
 
 
参考资料
  1. 26|ThreadLocal(上):线程安全的另类实现思路 (geekbang.org)
  1. 27|ThreadLocal(下):ThreadLocal家族成员及应用指南 (geekbang.org)
  1. 阿里面试,让我说说ThreadLocal,我一口气说了四种_Java_java金融_InfoQ写作社区
  1. 如何正确使用 ThreadLocal,你真的用对了吗? | 京东云技术团队_内存泄露_京东科技开发者_InfoQ写作社区
  1. Java内存区域详解(重点) | JavaGuide(Java面试 + 学习指南)
  1. 业务代码在线程池中乱使用java.lang.ThreadLocal变量,导致信息传递丢失的故障 (qq.com)
  1. java - ThreadLocal 内存泄漏问题深入分析 - 个人文章 - SegmentFault 思否
 
上一篇
在macOS上安装openCV用于Java调用(以4.7.0版本为例)
下一篇
UML类图再学习