尼恩说在前面
在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题:
1.请解释ThreadLocal是什么,以及它的主要用途是什么?
- ThreadLocal的内部机制是怎样的?请解释一下ThreadLocalMap和Entry。
3.使用ThreadLocal是否会导致内存泄漏?如果是,如何避免?
4.在使用线程池时,ThreadLocal可能会出现什么问题?如何解决?
5.能否解释一下 TransmittableThreadLocal 与ThreadLocal的区别和联系?
6.在父子线程间如何共享数据?ThreadLocal能实现吗?如果不能,那应如何实现?
最近有小伙伴在面试字节,又遇到了ThreadLocal 相关的面试题。小伙伴懵了,支支吾吾的说了几句,面试官不满意,面试挂了。
所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
这里,尼恩团队把 ThreadLocal、ITL、TTL、FTL进行了穿透式的梳理,
梳理为一个PDF文档 《ThreadLocal 学习圣经:一次穿透TL、ITL、TTL、FTL》, 并且持续迭代。
这个文档将成为大家 面试的杀手锏, 此文当最新PDF版本,可以找40岁老架构师尼恩获取。
当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V171版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,回复:领电子书
本文作者:
- 第一作者 Moen (负责写初稿 )
- 第二作者 尼恩 (40岁老架构师, 负责提升此文的 技术高度,让大家有一种 俯视 技术的感觉)
本文目录
什么是ThreadLocal(TL)?
在Java的多线程并发执行过程中,为保证多个线程对变量的安全访问,可以将变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立值,不会出现一个线程读取变量时而被另一个线程修改的现象。
ThreadLocal类通常被翻译为“线程本地变量
” ,或者“线程局部变量
” 。
ThreadLocal的英文字面翻译为“线程本地
”,实质上ThreadLocal代表的是线程本地变量,可能将其命名为ThreadLocalVariable
会更加容易让人理解。
以下来至官网的解释:
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).
简单翻译如下:
此类提供
线程局部变量
。这些变量与其正常对应变量的不同之处在于,每个访问一个变量(通过其get或set方法)的线程都有自己的
、独立初始化的变量副本
。ThreadLocal实例通常是类中的私有静态字段
,这些字段希望将状态与线程(例如,用户ID或事务ID)相关联。
总结重点如下:
-
ThreadLocal 提供了一种访问某个变量的特殊方式:访问到的变量属于当前线程,即保证每个线程的变量不一样,而同一个线程在任何地方拿到的变量都是当前这个线程私有的,这就是所谓的线程隔离。
-
如果要使用 ThreadLocal,通常定义为
private static
类型,根据编程范式最好是定义为private static final
类型。
ThreadLocal的基本使用
ThreadLocal是位于JDK的java.lang核心包中。
如果程序创建了一个ThreadLocal实例,那么在访问这个变量的值时,每个线程都会拥有一个独立的
、自己的本地值
。
“线程本地变量
” 可以看成专属于线程的变量,不受其他线程干扰,保存着线程的专属数据。
当线程结束后,每个线程所拥有的那一个本地值也会被释放。
在多线程并发操作“线程本地变量”时候,线程各自操作的是自己的本地值,从而规避了线程安全问题。
ThreadLocal如何是做到为每个线程存有一份独立的本地值的呢?
一个ThreadLocal实例可以形象地理解为一个Map(早期版本的ThreadLocal是这样设计的)。当工作线程Thread实例向本地变量保持某个值时,会以“Key-Value对”的形式保存在ThreadLocal内部的Map中,其中Key为线程Thread实例,Value为待保存的值。当工作线程Thread实例从ThreadLocal本地变量取值时,会以Thread实例为Key,获取其绑定的Value。
一个ThreadLocal实例内部结构的形象展示,大致如图1-18所示。
图1-18 一个ThreadLocal(早期版本)实例内部结构的形象展示
Java程序可以使用ThreadLocal的成员方法进行本地值操作,具体的成员方法如表1-2所示。
表1-2 ThreadLocal的成员方法
方 法 | 说 明 |
---|---|
set(T value) | 设置当前线程在“线程本地变量”实例中绑定的本地值 |
T get() | 获得当前线程在“线程本地变量”实例中绑定的本地值 |
remove() | 移除当前线程在“线程本地变量”实例中绑定的本地值 |
下面的例子,通过ThreadLocal的成员方法进行“线程本地变量”中线程本地值的设置、获取、移除,具体的代码如下:
package com.crazymakercircle.mutithread.basic.threadlocal;
...省略import
public class ThreadLocalTest
{
@Data
static class Foo
{
//实例总数
static final AtomicInteger AMOUNT = new AtomicInteger(0);
//对象的编号
int index = 0;
//对象的内容
int bar = 10;
//构造器
public Foo()
{
index = AMOUNT.incrementAndGet(); //总数增加,并且给对象的编号
}
@Override
public String toString()
{
return index + "@Foo{bar=" + bar + '}';
}
}
//定义线程本地变量
private static final ThreadLocal<Foo> LOCAL_FOO = new ThreadLocal<Foo>();
public static void main(String[] args) throws InterruptedException
{
//获取自定义的混合型线程池
ThreadPoolExecutor threadPool =
ThreadUtil.getMixedTargetThreadPool();
//提交5个任务,将会用到5个线程
for (int i = 0; i < 5; i++)
{
threadPool.execute(new Runnable()
{
@Override
public void run()
{
//获取“线程本地变量”中当前线程所绑定的值
if (LOCAL_FOO.get() == null)
{
//设置“线程本地变量”中当前线程所绑定的值
LOCAL_FOO.set(new Foo());
}
Print.tco("初始的本地值:" + LOCAL_FOO.get());
//每个线程执行10次
for (int i = 0; i < 10; i++)
{
Foo foo = LOCAL_FOO.get();
foo.setBar(foo.getBar() + 1); //值增1
sleepMilliSeconds(10);
}
Print.tco("累加10次之后的本地值:" + LOCAL_FOO.get());
//删除“线程本地变量”中当前线程所绑定的值
LOCAL_FOO.remove(); //这点对于线程池中的线程尤其重要
}
});
}
}
}
运行以上示例,其结果如下:
[apppool-1-mixed-3]:初始的本地值:3@Foo{bar=10}
[apppool-1-mixed-4]:初始的本地值:4@Foo{bar=10}
[apppool-1-mixed-5]:初始的本地值:5@Foo{bar=10}
[apppool-1-mixed-2]:初始的本地值:1@Foo{bar=10}
[apppool-1-mixed-1]:初始的本地值:2@Foo{bar=10}
[apppool-1-mixed-1]:累加10次之后的本地值:2@Foo{bar=20}
[apppool-1-mixed-3]:累加10次之后的本地值:3@Foo{bar=20}
[apppool-1-mixed-5]:累加10次之后的本地值:5@Foo{bar=20}
[apppool-1-mixed-2]:累加10次之后的本地值:1@Foo{bar=20}
[apppool-1-mixed-4]:累加10次之后的本地值:4@Foo{bar=20}
通过输出的结果可以看出,在“线程本地变量”(LOCAL_FOO)中,每一个线程都绑定了一个独立的值(Foo对象),这些值对象是线程的私有财产,可以理解为线程的本地值
每一次操作都是在自己的同一个本地值上进行,例子中通过线程本地值的index始终一致可以看出,每个线程操作的是同一个Foo对象。
如果线程尚未在本地变量(如LOCAL_FOO)中绑定了一个值,直接通过get( )方法去获取本地值,会获取到一个空值,此时可以通过set( )方法设置一个值作为初始值,具体的代码如下所示:
//获取“线程本地变量”中当前线程所绑定的值
if (LOCAL_FOO.get() == null)
{
//设置“线程本地变量”中当前线程所绑定的初始值
LOCAL_FOO.set(new Foo());
}
在当前线程尚未绑定值时,如果希望能从线程本地变量获取 初始值,而且也不想采用以上的“判空后设值”的繁琐 方式,则可以在定义使用 ThreadLocal.withInitial(…)
静态工厂方法,
使用ThreadLocal.withInitial(…) 的方式,可以在定义ThreadLocal对象时设置一个获取初始值的回调函数,具体的代码如下所示:
ThreadLocal<Foo> LOCAL_FOO = ThreadLocal.withInitial(() -> new Foo());
以上代码并没有使用new ThreadLocal()去构造一个ThreadLocal对象,而是使用withInitial(…)工厂方法创建一个ThreadLocal对象,并传递了一个获取初始值的Lamda回调函数。
在线程尚未绑定值而直接从“线程本地变量”获取值时,将会取得回调函数被调用之后所返回的值。
ThreadLocal的成员方法
Java程序可以使用ThreadLocal的成员方法进行本地值操作,具体的成员方法如下所示:
set(T value) :设置当前线程在“线程本地变量”实例中绑定的本地值
T get() :获得当前线程在“线程本地变量”实例中绑定的本地值
remove() :移除当前线程在“线程本地变量”实例中绑定的本地值
initialValue( ) : 当“线程本地变量”在当前线程的ThreadLocalMap中尚未绑定值时,该方法用于获取初始值。
ThreadLocal的作用和优劣势
ThreadLocal的作用和优劣势主要体现在以下几个方面:
作用
-
线程本地存储:ThreadLocal为每个线程提供各自的变量副本,每个线程都可以读取和修改自己线程的本地变量。这意味着在多线程环境中,不同线程对ThreadLocal变量的操作是独立的,不会互相干扰。
-
简化线程间数据传递:通过使用ThreadLocal,可以将某些需要在多线程之间共享但又需要避免竞态条件的数据封装起来,每个线程访问的都是自己的数据副本,从而简化了线程间数据传递的复杂性。
-
管理线程特定资源:ThreadLocal常用于存储线程上下文信息,如用户会话信息、事务信息等,这些信息通常与特定线程关联,不需要在多个线程之间共享。
优势
-
线程安全:由于每个线程操作的是自己的变量副本,因此避免了多线程访问共享变量时可能出现的竞态条件和数据不一致问题,从而保证了线程安全。
-
简化编程模型:通过使用ThreadLocal,开发者可以更加专注于业务逻辑的实现,而不需要过多关注线程间数据同步和共享的问题,降低了编程复杂度。
-
性能优化:由于避免了线程间数据同步的开销,以及减少了不必要的锁竞争,因此在某些场景下,使用ThreadLocal可以提高系统的并发性能。
劣势
-
内存消耗:ThreadLocal为每个线程创建变量副本,这意味着当线程数量较多时,会占用较多的内存资源。特别是在长时间运行的系统中,如果线程频繁创建和销毁,可能会导致内存泄漏问题。
-
数据共享限制:由于ThreadLocal变量是线程私有的,因此无法直接实现线程间的数据共享。如果需要在线程间传递数据,可能需要借助其他机制(如消息队列、共享内存等)。
-
使用不当可能导致内存泄露问题:如果开发者在使用ThreadLocal时不小心忘记在线程结束后清理变量(例如通过调用
remove()
方法),那么这些变量可能会一直存在于内存中,造成内存泄漏。此外,如果多个线程需要访问和修改同一份数据,那么ThreadLocal可能并不适合,因为它提供的是每个线程私有的变量副本。 -
性能开销:在ThreadLocal中ThreadLocalMap 是一种使用线性探测法实现的哈希表,底层采用数组存储数据。ThreadLocal.set()/get() 方法在数据密集时很容易出现 Hash 冲突,hash冲突使用的是线性探测法,需要 O(n) 时间复杂度解决冲突问题,效率较低。
综上所述
ThreadLocal在简化线程间数据传递、管理线程特定资源和提高线程安全性方面具有优势,但也需要注意其可能带来的内存消耗和数据共享限制等问题。在使用ThreadLocal时,应根据具体的应用场景和需求进行权衡和选择。
ThreadLocal的使用场景
ThreadLocal是解决线程安全问题一个较好方案,它通过为每个线程提供一个独立的本地值,去解决并发访问的冲突问题。很多情况下,使用ThreadLocal比直接使用同步机制(如synchronized
)解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
ThreadLocal使用场景,大致可以分为以下两类:
-
线程隔离
- ThreadLocal的主要价值在于线程隔离,ThreadLocal中数据只属于当前线程,其本地值对别的线程是不可见的,在多线程环境下,可以防止自己的变量被其他线程篡改。另外,由于各个线程之间的数据相互隔离,避免同步加锁带来的性能损失,大大提升了并发性的性能。
- ThreadLocal在线程隔离的最常用案例为:可以每个线程绑定一个用户会话信息、数据库连接、HTTP请求等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
- 常见的ThreadLocal使用场景为数据库连接独享、Session数据管理等场景在“线程隔离”场景中使用ThreadLocal的典型案例为:可以每个线程绑定一个数据库连接,是的这个数据库连接为线程所独享,从而避免数据库连接被混用而导致操作异常问题。
-
跨函数传递数据
- 通常用于同一个线程内,跨类、跨方法传递数据时,如果不用ThreadLocal,那么相互之间的数据传递势必要靠返回值和参数,这样无形之中增加了这些类或者方法之间的耦合度。、
场景1:使用ThreadLocal进行线程隔离
ThreadLocal在“线程隔离”应用场景的典型应用为“数据库连接独享”。下面的代码来自Hibernate,代码中通过ThreadLocal进行数据库连接(Session)的“线程本地化”存储,主要的代码如下:
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
Hibernate
对数据库连接进行了封装,一个Session 代表一个数据库连接。通过以上代码可以看到,
在Hibernate的getSession()方法中,首先判断当前线程中有没有放进去session,
如果还没有,那么通过sessionFactory().openSession()来创建一个Session ,再将Session 设置到ThreadLocal变量中,这个Session 相当于线程的私有变量,而不是所有线程共用的,显然其他线程中是取不到这个Session。
一般来说,完成数据库操作之后程序会将Session
关闭,从而节省数据库连接资源。如果Session的使用方式为共享而不是独占,在这种情况下,Session是多线程共享使用的,如果某个线程使用完成之后,直接将Session关闭,其他线程在操作Session就会报错。所以Hibernate通过ThreadLocal非常简单实现了数据库连接的安全使用。
场景2:使用ThreadLocal进行跨函数数据传递
ThreadLocal在“跨函数数据传递
”应用场景的典型有很多:
用来传递请求过程中的用户ID。
用来传递请求过程中的用户会话(Session)。
用来传递HTTP的用户请求实例HttpRequest。
其他需要在函数之间频繁传递的数据。
以下代码来自于疯狂创客圈社群的微服务脚手架Crazy-SpringCloud
工程,通过ThreadLocal在函数之间传递用户信息、会话信息等,并且封装成了一个独立的SessionHolder类,具体的代码如下:
package com.crazymaker.springcloud.common.context;
...省略import
public class SessionHolder
{
// session id 线程本地变量
private static final ThreadLocal<String> sidLocal =
new ThreadLocal<>("sidLocal");
// 用户信息 线程本地变量
private static final ThreadLocal<UserDTO> sessionUserLocal =
new ThreadLocal<>("sessionUserLocal");
// session 线程本地变量
private static final ThreadLocal<HttpSession> sessionLocal =
new ThreadLocal<>("sessionLocal");
...省略其他
/**
*保存session在线程本地变量中
*/
public static void setSession(HttpSession session)
{
sessionLocal.set(session);
}
/**
* 取得绑定在线程本地变量中的session
*/
public static HttpSession getSession()
{
HttpSession session = sessionLocal.get();
Assert.notNull(session, "session 未设置");
return session;
}
...省略其他
}
场景3:ThreadLocal在Java框架中的应用
- Spring
- 在Spring框架中,ThreadLocal用于存储数据库连接等线程特定的资源。由于数据库连接是线程不安全的,因此每个线程都需要有自己的连接副本。Spring通过ThreadLocal将数据库连接与当前线程关联起来,从而避免了多线程环境下的数据竞争和不一致问题。
- 在Spring的事务管理中,ThreadLocal也扮演着重要角色。它确保了每个线程都有自己的事务上下文,包括事务状态、回滚点等信息,从而实现了事务的隔离性。
- MyBatis
- MyBatis是一个优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。在MyBatis中,ThreadLocal可以用来存储SqlSession对象。由于SqlSession不是线程安全的,因此每个线程都应该拥有自己独立的SqlSession实例。通过ThreadLocal,MyBatis可以方便地实现SqlSession的线程局部存储,确保每个线程都能正确地执行SQL操作。
- 分布式系统
- 在分布式系统中,ThreadLocal可以用来传递全局ID和分支ID等关键信息。这些ID对于分布式事务的追踪和诊断至关重要。通过将这些ID存储在ThreadLocal中,可以确保它们在整个请求处理过程中都能被正确传递和使用。
- 日志框架
- 一些日志框架也利用ThreadLocal来存储与当前线程相关的日志上下文信息,如用户ID、操作类型等。这样,在记录日志时,可以方便地获取这些信息,并将其添加到日志条目中,从而方便后续的日志分析和排查问题。
- RPC
- 在远程过程调用(RPC)框架中,ThreadLocal用于存储和传递与当前调用相关的上下文信息。这些上下文信息可能包括调用者的身份、调用的参数、超时设置等。通过将这些信息存储在ThreadLocal中,可以确保它们在RPC调用过程中能够被正确地传递和使用。
- Hibernate
- SessionContext: 用于存储当前线程的Hibernate会话相关数据,如当前会话、持久化上下文等。
- TransactionManager: 管理事务状态,每个线程可以有独立的事务状态,如当前是否在事务中。
- Tomcat
- ThreadLocal变量: 用于跟踪每个请求的会话信息、用户认证数据等,确保这些数据不会在请求之间共享。
- Kafka
- Producer and Consumer Threads: 在消息生产和消费过程中,使用ThreadLocal来存储线程特定的配置和状态信息。
ThreadLocal综合使用案例
由于ThreadLocal使用不当会导致严重的内存泄漏,所以为了更好的避免内存泄漏的发生,我们使用ThreadLocal时遵守以下两个原则:
-
尽量使用private static final修饰ThreadLocal实例。使用 private 与final 修饰符,主要是尽可能不让他人修改、变更ThreadLocal变量的引用; 使用static 修饰符主要为了确保ThreadLocal实例的全局唯一。
-
ThreadLocal使用完成之后务必调用remove方法。这是简单、有效地避免ThreadLocal引发内存泄漏的方法。
下面用一个综合案例演示一下ThreadLocal的使用。此案例的功能为:记录执行过程中所调用的函数的执行耗时。比如在实际Web开发过程中,一次客户端请求往往会涉及到DB、缓存、RPC等多个耗时调用,一旦出现性能问题,就需要记录一下各个点耗时的时间,从而判断性能的瓶颈所在。
下面的代码定义了三个方法serviceMethod
、daoMethod
、rpcMethod
,用于模拟实际的DB、RPC等耗时调用,具体的代码如下:
package com.crazymakercircle.mutithread.basic.threadlocal;
...省略import
public class ThreadLocalTest2
{
/**
* 模拟业务方法
*/
public void serviceMethod()
{
//睡眠500ms,模拟执行耗时
sleepMilliSeconds(500);
//记录从开始调用到当前这个点( "point-1")的耗时
SpeedLog.logPoint("point-1 service");
//调用DAO方法:模拟dao业务方法
daoMethod();
//调用RPC方法:模拟RPC远程业务方法
rpcMethod();
}
/**
* 模拟dao业务方法
*/
public void daoMethod()
{
//睡眠400ms,模拟执行耗时
sleepMilliSeconds(400);
//记录上一个点("point-1")这里("point-2")的耗时
SpeedLog.logPoint("point-2 dao");
}
/**
* 模拟RPC远程业务方法
*/
public void rpcMethod()
{
//睡眠400ms,模拟执行耗时
sleepMilliSeconds(600);
//记录上一个点("point-2")这里("point-3")的耗时
SpeedLog.logPoint("point-3 rpc");
}
...省略不相干代码
}
为了能灵活地记录各个执行埋点的耗时,这里定义了一个SpeedLog类。该类含有一个ThreadLocal类型的、初始值为一个Map<String, Long>实例的“线程本地变量”,名字叫做TIME_RECORD_LOCAL。
如果要记录某个函数的调用耗时,就需要进行耗时埋点,具体的方法为logPoint(String point)。该方法会操作TIME_RECORD_LOCAL本地变量,在其中增加一次耗时记录:Key为耗时埋点的名称,值为当前时间和上一次记录时间的差值,也就是上一次埋点到本次埋点之间的调用耗时。
SpeedLog类的代码,大致如下:
package com.crazymakercircle.mutithread.basic.threadlocal;
...省略import
public class SpeedLog
{
/**
* 记录调用耗时的本地Map变量
*/
private static final ThreadLocal<Map<String, Long>>
TIME_RECORD_LOCAL =ThreadLocal.withInitial(SpeedLog::initialStartTime);
/**
* 记录调用耗时的本地Map变量的初始化方法
*/
public static Map<String, Long> initialStartTime()
{
Map<String, Long> map = new HashMap<>();
map.put("start", System.currentTimeMillis());
map.put("last", System.currentTimeMillis());
return map;
}
/**
* 开始耗时记录
*/
public static final void beginSpeedLog()
{
Print.fo("开始耗时记录");
TIME_RECORD_LOCAL.get();
}
/**
* 结束耗时记录
*/
public static final void endSpeedLog()
{
TIME_RECORD_LOCAL.remove();
Print.fo("结束耗时记录");
}
/**
* 耗时埋点
*/
public static final void logPoint(String point)
{
//获取上一次的时间
Long last = TIME_RECORD_LOCAL.get().get("last");
//计算上一次埋点到当前埋点的耗时
Long cost = System.currentTimeMillis() - last;
//保存上一次埋点到当前埋点的耗时
TIME_RECORD_LOCAL.get().put(point + " cost:", cost);
//保存当前时间,供下一次埋点使用
TIME_RECORD_LOCAL.get().put("last", System.currentTimeMillis());
}
...省略不相干代码
}
下面是一个测试用例,演示一下在 serviceMethod、daoMethod、rpcMethod三个模拟方法的调用过程中,其耗时的记录和输出,具体的代码如下:
package com.crazymakercircle.mutithread.basic.threadlocal;
...省略import
public class ThreadLocalTest2
{
/**
* 测试用例:线程方法调用的耗时
*/
@org.junit.Test
public void testSpeedLog() throws InterruptedException
{
Runnable runnable = () ->
{
//开始耗时记录,保存当前时间
SpeedLog.beginSpeedLog();
//调用模拟业务方法
serviceMethod();
//打印耗时
SpeedLog.printCost();
//结束耗时记录
SpeedLog.endSpeedLog();
};
new Thread(runnable).start();
sleepSeconds(10);//等待10s看结果
}
...省略不相干代码
}
运行以上用例,三个模拟方法 serviceMethod、daoMethod、rpcMethod的耗时输出如下:
[SpeedLog.beginSpeedLog]:开始耗时记录
[SpeedLog.printCost]:start =>1600347227334
[SpeedLog.printCost]:point-1 service cost: =>500
[SpeedLog.printCost]:point-2 dao cost: =>401
[SpeedLog.printCost]:point-3 rpc cost: =>600
[SpeedLog.printCost]:last =>1600347228835
[SpeedLog.endSpeedLog]:结束耗时记录
以上案例中,将ThreadLocal变量声明成为private static final
的形式,使得外部不能直接访问,外部能访问的是将ThreadLocal变量封装之后的接口函数如beginSpeedLog( )、logPoint(String point)、endSpeedLog( )等等。
总之,使用ThreadLocal能实现每个线程都有一份变量的本地值,其原因是由于每个线程都有自己独立的ThreadLocalMap空间,本质上属于以空间换时间的设计思路,该设计思路属于了另一种意义的 “无锁编程”。
ThreadLocal 使用总结
总结起来,ThreadLocal在实际项目中的应用广泛且实用,但同时也需注意其潜在的风险和挑战。通过遵循最佳实践,合理设计和管理ThreadLocal变量,我们能够在多线程环境下高效地解决数据隔离问题,同时保持代码简洁易懂,确保系统稳定性和高性能运行。
ThreadLocal的实现原理
在早期的JDK版本中,ThreadLocal的内部结构是一个Map,其中每一个线程实例作为Key,线程在“线程本地变量”中绑定的值为Value(本地值)。早期版本中的Map结构,其拥有者为ThreadLocal,每一个ThreadLocal实例,拥有一个Map实例。
在JDK8版本中,ThreadLocal的内部结构发生了演进,虽然还是使用了Map结构,但是Map结构的拥有者已经发生了变化,其拥有者为Thread(线程)实例,每一个Thread实例,拥有一个Map实例。另外,Map结构的Key值也发生了变化:新的Key为ThreadLocal实例。
在JDK8版本中,每一个Thread线程内部都有一个Map(ThreadLocalMap),因为如我们给一个Thread创建多个Threadlocal实列,然乎放置本地数据,那么当前线程的ThreadLocalMap中就会有多个“Key-Value对”,其中ThreadLocal实例为key,本地数据为Value。
在代码的层面来说,新版本的ThreadLocalMap还是由ThreadLocal类维护的,由ThreadLocal负责ThreadLocalMap实例的获取、创建,并从中设置本地值、获取本地值。所以ThreadLocalMap还寄存于ThreadLocal内部,而并没有被迁移到Thread内部。
ThreadLocal内部使用ThreadLocalMap来保存每个线程的变量副本。每个Thread都持有一个ThreadLocalMap的引用,这个map的key是ThreadLocal实例本身,value是线程变量副本。
当线程首次调用ThreadLocal的get()或set()方法时,ThreadLocalMap会被创建并关联到当前线程。此后,线程就可以通过ThreadLocal实例的get()和set()方法访问自己的线程局部变量了。
简单来说:ThreadLocal 就是一种以空间换时间
的做法,在每个 Thread 里面维护了一个以开放定址法实现的ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。
ThreadLocal内部结构演进
在早期的JDK版本中,ThreadLocal的内部结构是一个Map,其中每一个线程实例作为Key,线程在“线程本地变量”中绑定的值为Value(本地值)。早期版本中的Map结构,其拥有者为ThreadLocal
,每一个ThreadLocal实例,拥有一个Map
实例。
在JDK8版本中,ThreadLocal的内部结构发生了演进,虽然还是使用了Map结构,但是Map结构的拥有者已经发生了变化,其拥有者为Thread(线程)实例
,每一个Thread实例,拥有一个Map
实例。另外,Map结构的Key值也发生了变化:新的Key为ThreadLocal实例
。
在JDK8版本中,每一个Thread线程内部都有一个Map(ThreadLocalMap
),因为如我们给一个Thread创建多个Threadlocal实列,然乎放置本地数据,那么当前线程的ThreadLocalMap中就会有多个“Key-Value对”,其中ThreadLocal实例为key
,本地数据为Value
。
在代码的层面来说,新版本的ThreadLocalMap还是由ThreadLocal类维护的,由ThreadLocal负责ThreadLocalMap实例的获取、创建,并从中设置本地值、获取本地值。
所以ThreadLocalMap还寄存于ThreadLocal内部,而并没有被迁移到Thread内部。
每一个线程在获取本地值时,将ThreadLocal实例作为Key从自己拥有的ThreadLocalMap中获取值,别的线程无法访问自己的ThreadLocalMap实例,自己也无法访问别人ThreadLocalMap实例,达到相互隔离,互不干扰。
jdk1.8版本ThreadLocalMap 与早期版本的ThreadLocalMap实现相比,主要的变化为:
-
拥有者发生了变化:新版本的ThreadLocalMap拥有者为
Thread
,早期版本的ThreadLocalMap拥有者为ThreadLocal
。 -
Key发生了变化:新版本的Key为
ThreadLocal
实例,早期版本的Key为Thread
实例。
jdk1.8版本ThreadLocalMap 与早期版本的ThreadLocalMap实现相比,新版本的主要优势为
:
(1)每个ThreadLocalMap存储的“Key-Value对”数量变少。
-
早期版本的“Key-Value对”数量与线程个数强关联,如果线程数量多,则ThreadLocalMap存储“Key-Value对”数量也多。
-
新版本的ThreadLocalMap的Key 为ThreadLocal实例,多线程情况下ThreadLocal实例比线程数少。
(2)ThreadLocalMap拥有者为Thread,降低内存消耗。
- 早期版本ThreadLocalMap的拥有者为ThreadLocal,在Thread(线程)实例销毁后,ThreadLocalMap还是存在的;
- 新版本的ThreadLocalMap的拥有者为Thread,现在当Thread实例销毁后,ThreadLocalMap也会随之销毁,在一定程度上能减少内存的消耗。
ThreadLocalMap对象和Entry是什么
ThreadLocal的操作都是基于ThreadLocalMap
展开的,而ThreadLocalMap
是ThreadLocal的一个静态内部类,其实现了一套简单的Map结构(比HashMap简单)。
ThreadLocalMap内部维护Entry数组,作为ThreadLocalMap条目数组,作为散列表使用,如下图:
ThreadLocal的结构模型
以jdk1.8的ThreadLocal 为标准, 总结一下 ThreadLocal的结构模型。
每个Thread都有一个 ThreadLocalMap 结构,其中就保存着当前线程所持有的所有 ThreadLocal。
ThreadLocal 本身只是一个引用,没有直接保存值,值是保存在 ThreadLocalMap 中,ThreadLocal 作为 key,值(实际保存的数据)作为 value。
可以用下面的图来概括:
ThreadLocal源码分析
ThreadLocal的源码提供的方法不多,主要的方法有:set(T value)方法
、get( )方法
、remove( )方法
、initialValue( )方法
。
set(T value)方法
set(T value)方法
用于设置“线程本地变量”在当前线程的ThreadLocalMap中对应的值,相当于设置线程本地值,其核心源码如下:
public void set(T value) {
//获取当前线程对象
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap 成员
ThreadLocalMap map = getMap(t);
//判断map是否存在
if (map != null)
{
//value被绑定到threadLocal实例
map.set(this, value);
}
else
{
// 如果当前线程没有ThreadLocalMap成员实例
// 创建一个ThreadLocalMap实例,然后,作为成员关联到t(thread实例)
createMap(t, value);
}
}
// 获取线程t的ThreadLocalMap成员
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 线程t创建一个ThreadLocalMap成员
//并为新的Map成员设置第一个Key-Value对,Key为当前的ThreadLocal实例
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
通过以上的源码,可以看出set(T value)方法
的执行流程,大致如下:
- 获得当前线程,然后获得当前线程的ThreadLocalMap成员,暂存于map变量。
- 如果map不为空,则将Value设置到map中,当前的Threadlocal作为key。
- 如果map为空,给该线程创建map,然后设置第一个“Key-Value对”,Key为当前的ThreadLocal实例,Value为set方法的参数value值。
get( )方法
get( )方法
用于获取“线程本地变量”在当前线程的ThreadLocalMap
中对应的值,相当于获取线程本地值,其核心源码如下:
public T get() {
//获得当前线程对象
Thread t = Thread.currentThread();
//获得线程对象的ThreadLocalMap 内部成员
ThreadLocalMap map = getMap(t);
// 如果当前线程的内部map成员存在
if (map != null) {
// 以当前threadlocal为Key,尝试获得条目
ThreadLocalMap.Entry e = map.getEntry(this);
// 条目存在
if (e != null) {
T result = (T)e.value;
return result;
}
}
// 如果当前线程对应map不存在
//或者map存在,但是当前threadlocal实例没有对应的Key-Value,返回初始值
return setInitialValue();
}
// 设置threadlocal关联的初始值并返回
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;
}
通过以上的源码,可以看出T get()方法
的执行流程,大致如下:
- 先尝试获得当前线程,然后获得当前线程的ThreadLocalMap成员,暂存于map变量。
- 如果获得的map不为空,以当前threadlocal实例为Key尝试获得map中的Entry(条目)。
- 如果Entry条目不为空,返回Entry中的Value。
- 如果Entry为空,则通过调用initialValue初始化钩子函数获取“ThreadLocal”初始值,并设置在map中。如果map不存在,还会给当前线程创建新ThreadLocalMap成员,并绑定第一个“Key-Value对”。
remove( )方法
remove()方法
用于在当前线程的ThreadLocalMap中,移除“线程本地变量”所对应的值,其核心源码如下:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
initialValue( ) 方法
当“线程本地变量”在当前线程的ThreadLocalMap
中尚未绑定值时,initialValue( )方法
用于获取初始值。其源码如下:
protected T initialValue() {
return null;
}
如果没有调用set直接调用get,则会调用此方法,但是该方法只会被调用一次。
默认情况下,initialValue( )方法返回null,如果不想返回null,可以继承ThreadLocal覆盖此方法。
真的需要继承ThreadLocal去重写initialValue()方法吗?其实没有必要。
JDK已经为大家定义了一个ThreadLocal的内部SuppliedThreadLocal静态子类,并且提供了 ThreadLocal.withInitial(…)
静态工厂方法,方便大家在定义ThreadLocal实例时设置初始值回调函数。
使用工厂方法构造ThreadLocal实例的代码如下:
ThreadLocal<Foo> LOCAL_FOO = ThreadLocal.withInitial(() -> new Foo());
JDK定义的ThreadLocal.withInitial(…)
静态工厂方法,以及其内部子类SuppliedThreadLocal的源码如下:
//ThreadLocal工厂方法,可以设置本地变量初始值钩子函数
public static <S> ThreadLocal<S> withInitial(
Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
//内部静态子类
//继承了ThreadLocal,重写了initialValue()方法,返回钩子函数的值作为初始值
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
//保存钩子函数
private final Supplier<? extends T> supplier;
//传入钩子函数
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
@Override
protected T initialValue() {
return supplier.get(); //返回钩子函数的值作为初始值
}
}
ThreadLocalMap源码分析
ThreadLocal的操作都是基于ThreadLocalMap
展开的,而ThreadLocalMap
是ThreadLocal的一个静态内部类,其实现了一套简单的Map结构(比HashMap简单)。
ThreadLocalMap的主要成员变量
ThreadLocalMap
的成员变量与HashMap的成员变量非常类似,其内部的主要成员如下所示:
public class ThreadLocal<T> {
...省略其他
static class ThreadLocalMap {
//Map的条目数组,作为散列表使用
private Entry[] table;
//Map的条目初始容量16
private static final int INITIAL_CAPACITY = 16;
//Map的条目数量
private int size = 0;
//扩容因子
private int threshold;
//Map的条目类型,一个静态的内部类
// Entry 继承子WeakReference,Key为ThreadLocal实例
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value; //条目的值
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...省略其他
}
ThreadLocal源码中get()
、set( )
、remove()方法
都涉及到ThreadLocalMap的方法调用,主要调用了ThreadLocalMap的如下几个函数:
- set(ThreadLocal<?> key, Object value) :向Map实例设置“Key-Value对”。
- getEntry(ThreadLocal):从Map实例获取Key(ThreadLocal实例)所属的Entry。
- remove(ThreadLocal):根据Key(ThreadLocal实例)从Map实例移除所属的Entry。
作为参考,这里只将ThreadLocalMap
的set(ThreadLocal<?> key, Object value) 方法
的代码以注释的形式做一个简单的分析,具体如下:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//根据key的HashCode,找到key在数组上的槽点i
int i = key.threadLocalHashCode & (len-1);
// 从槽点i开始向后循环搜索,找空余槽点(空余位置)或者找现有槽点
//如果没有现有槽点,则必定有空余槽点,因为没有空间时会扩容
for (Entry e = tab[i]; e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//找到现有槽点:Key值为ThreadLocal实例
if (k == key) {
e.value = value;
return;
}
//找到异常槽点:槽点被GC掉,重设Key值和Value值
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//没有找到现有的槽点,增加新的Entry
tab[i] = new Entry(key, value);
//设置ThreadLocal数量
int sz = ++size;
//清理Key为null的无效Entry
//没有可清理的Entry,并且现有条目数量大于扩容因子值,进行扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
Entry的Key需要使用弱引用
Entry
用于保存ThreadLocalMap的“Key-Value
”条目,但是Entry使用了对Threadlocal实例进行包装之后的弱引用(WeakReference
)作为Key,其代码如下:
// Entry 继承了WeakReference,并使用WeakReference对Key进行包装
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value; //值
Entry(ThreadLocal<?> k, Object v) {
super(k); //使用WeakReference对Key值进行包装
value = v;
}
}
为什么Entry需要使用弱引用对Key进行包装,而不是直接使用Threadlocal实例作为Key呢?
这个问题有点儿复杂,如果要分析清楚还有点难度。这里从一个简单的例子入手,假设有一个方法funcA( )创建了一个“线程本地变量”,具体如下:
public void funcA()
{
//创建一个线程本地变量
ThreadLocal local = new ThreadLocal<Integer>();
//设置值
local.set(100);
//获取值
local.get();
//函数末尾
}
当线程tn执行funcA方法到其末尾时,线程tn相关的JVM栈内存以及内部ThreadLocalMap成员的结构,大致如图1-20所示。
线程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回收
,这将造成严重的内存泄露
,具体如图1-21所示。
图1-20 当线程tn执行funcA方法末尾时内存结构
图1-21 若Entry的Key为强引用将导致ThreadLocal实例不能回收
什么是弱引用呢?
仅有弱引用(WeakReference)指向的对象,只能生存到下一次垃圾回收之前。
换句话说,当GC发生时,不管内存够不够,仅有弱引用所指向的对象都会被回收。而拥有强引用指向的对象,则不会被直接回收。
什么是内存泄漏呢?
不再用到的内存,没有及时释放,就叫做内存泄漏。
对于持续运行的服务进程,必须及时释放内存,否则内存占用率越来越高,轻则影响系统性能,重则导致进程崩溃。
由于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操作。
只要存在一个针对任何ThreadLocal实例的get、set或remove操作,就会触发Thread实例拥有的ThreadLocalMap的Key为null的Entry清理工作,释放掉ThreadLocal弱引用为null的Entry。
综合以上两条可以看出:使用ThreadLocal出现内存泄漏还是比较容易的。但是一般公司对如何使用ThreadLocal都有编程规范要求,只要大家按照规范编写程序,也没有那么容易发生内存泄漏。
ThreadLocal造成内存泄露的问题
什么是内存泄漏?
不再用到的内存,没有及时释放,就叫做内存泄漏。
对于持续运行的服务进程,必须及时释放内存,否则内存占用率越来越高,轻则影响系统性能,重则导致进程崩溃。
ThreadLocal是怎么造成内存泄露的呢?
如果发生了下面的情况:
-
如果ThreadLocal是null了,也就是要被GC回收了,
-
但是此时我们的ThreadLocalMap(thread 的内部属性)生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。
总之,就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。
我们细致的分析一下。
ThreadLocal 有两个引用链
ThreadLocalMap中的Key就是ThreadLocal对象,ThreadLocal 有两个引用链:
- 一个引用链是栈内存中ThreadLocal引用:
- 一个引用链是ThreadLocalMap中的Key对它的引用:
而对于Value(实际保存的值)来说,它的引用链只有一条,就是从Thread对象引用过来的,如下图:
上述过程分析后,就会出现如下的两种情况:
情况1: key的泄漏
情况2: value的泄漏
情况1:key的泄漏
栈上的ThreadLocal Ref引用不再使用了,即当前方法结束处理后,这个对象引用就不再使用了,
那么,ThreadLocal对象因为还有一条引用链存在,如果是强引用的话,这里就会导致ThreadLocal对象无法被回收,可能导致OOM。
情况1 的解决方案,使用弱引用解决 。
情况2: value的泄漏
情况2.假设我们使用了线程池,如果Thread对象一直被占用使用中(如在线程池中被重复使用
),但是此时我们的ThreadLocalMap(thread 的内部属性)生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。
这就意味着,Value这条引用链就一直存在,那么就会导致ThreadLocalMap无法被JVM回收,可能导致OOM,如上图。
情况2 ,比较严重。还得另想办法。
情况1的解决方案:使用弱引用,解决key的内存泄露
从如下ThreadLocal中内部类Entry代码可知:
Entry类的父类是弱引用WeakReference,ThreadLocal的引用k通过 WeakReference 构造方法传递给了 父类WeakReference的构造方法,
从而,ThreadLocalMap中的Key是ThreadLocal的弱引用,通过弱引用来解决内存泄露问题。
具体的代码如下
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // key为弱引用
value = v;
}
}
}
栈内存中的ThreadLocal Ref引用不再使用了,即当当前方法结束处理后,这个key对象引用就不再使用了,
那么,如果这里 不用弱引用而是强引用的话,这里ThreadLocal对象因为还有一条引用链存在,所以就会导致他无法被回收,可能导致OOM。
回顾Java中4种引用类型
- 强引用(Strong Reference):
- 这是最常见的引用类型。一个对象具有强引用,垃圾收集器就不会回收它,即使系统内存空间不足。
- 示例:
Object obj = new Object();
在这里,obj
就是new Object()
的一个强引用。- 软引用(Soft Reference):
- 用来描述一些可能还有用但并非必需的对象。在系统将要发生内存溢出异常前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
- 在Java中,软引用是用来实现内存敏感的高速缓存。
- 示例:使用
java.lang.ref.SoftReference
类可以创建软引用。- 弱引用(Weak Reference):
- 这里讨论ThreadLocalMap中Entry类的重点。
- 弱引用也是用来描述非必需对象的,它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收被弱引用关联的对象。
- 在Java中,弱引用是用来描述那些非关键的数据,在Java里用
java.lang.ref.WeakReference
类来表示。- 示例:使用
java.lang.ref.WeakReference
类可以创建弱引用。- 虚引用(Phantom Reference):
- 一个虚引用关联着的对象,在任何时候都可能被垃圾收集器回收,它不能单独用来获取被引用的对象。虚引用必须和引用队列(
ReferenceQueue
)联合使用。主要用来跟踪对象被垃圾回收的活动。- 虚引用对于一般的应用程序来说意义不大,主要使用在能比较精确控制Java垃圾收集器的高级场景中。
- 示例:使用
java.lang.ref.PhantomReference
类可以创建虚引用。
弱引用也是用来描述非必需对象的,它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收被弱引用关联的对象。
从而解决 key的泄漏问题。
情况2的解决方案:清理策略解决value内存泄露
为了解决value内存泄露问题,Java 的 ThreadLocal 实现了两大清理方式:
- 探测式清理(Proactive Cleanup)
- 启发式清理(Heuristic Cleanup) 。
源码:value的 探测式清理 :
当线程调用 ThreadLocal
的 get()
、set()
或 remove()
方法时,会触发对 ThreadLocalMap 的清理。
此时,ThreadLocalMap 会检查所有键(ThreadLocal 实例),并移除那些已经被垃圾回收的key键及其对应的value 值。
这种清理是主动的,因为它是在每次操作 ThreadLocal 时进行的。
探测式清理(Proactive Cleanup)如何实现的呢?:
从当前节点开始遍历数组,将key等于null的entry置为null,key不等于null则rehash重新分配位置,若重新分配上的位置有元素则往后顺延。
注意:这里把清理的开销放到了get、set操作上,如果get的时候无用Entry(Entry的Key为null)特别多,那这次get相对而言就比较慢了。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
// 将k=null的entry置为null
e.value = null;
tab[i] = null;
size--;
} else {
// k不为null,则rehash从新分配配置
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
// 重新分配后的位置上有元素则往后顺延。
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
源码:value的启发式清理:
在 ThreadLocalMap
的 set() 方法中,有一个阈值(默认为 ThreadLocalMap.Entry 数组长度的 1/4)。
当 ThreadLocalMap 中的 Entry 对象被删除(通过键的弱引用被垃圾回收)并且剩余的 Entry 数量大于这个阈值时,会触发一次启发式清理操作。
这种清理是启发式的,因为它不是每次操作都进行,而是基于一定的条件和概率。
启发式清理(Heuristic Cleanup)如何实现?:
从当前节点开始,进行do-while循环检查清理过期key,结束条件是连续n次未发现过期key就跳出循环,n是经过位运算计算得出的,可以简单理解为数组长度的2的多少次幂次。
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
// 移除
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
业务主动清理:手动清除解决内存泄露
尽管有弱引用以及这些清理机制,但最佳实践业务主动清理,
如何业务主动清理?在使用完 ThreadLocal 后显式调用 remove()方法
,以确保不再需要的值能够被及时回收,key和value 都同时清理,一锅端。
这样可以避免潜在的内存泄漏问题,并减少垃圾回收的压力。
ThreadLocal与内存泄露:防范与诊断
ThreadLocal的一个常见问题是内存泄露。
这通常发生在使用线程池的场景中,因为线程池中的线程通常是长期存在的,它们的ThreadLocal变量也不会自动清理,这可能导致内存泄漏。
JDK 用了三个办法,来解决内存泄漏。
业务上解决这个问题的一个方法是,每当使用完ThreadLocal变量后,显式地调用remove()
方法来清除它:
使用ThreadLocal的性能问题和优化措施
虽然ThreadLocal提供了很方便的线程隔离机制,但有性能损耗的。
ThreadLocal的性能开销
ThreadLocal的性能开销主要来自两个方面:
- ThreadLocalMap的维护。
- ThreadLocal变量的创建和销毁。
在使用ThreadLocal时,尤其是在高并发的环境下,要注意其对性能的影响。
因此,在使用ThreadLocal时,要尽量复用、重用ThreadLocal变量,避免在高频率的操作中频繁地创建和销毁它们。`
编程规范:推荐使用 static final 修饰ThreadLocal对象
如何 要尽量复用、重用ThreadLocal变量?
编程规范有云:ThreadLocal 实例作为ThreadLocalMap的Key,针对一个线程内所有操作是共享的,所以建议设置static修饰符,以便被所有的对象共享。
由于静态变量会在类第一次被使用时装载,只会分配一次存储空间,此类的所有实例都会共享这个存储空间,所以使用 static
修饰ThreadLocal 就会节约内存空间。
另外,为了确保ThreadLocal 实例的唯一性,除了使用static修饰之外,还会使用final
进行加强修饰,以防止其在使用过程中发生动态变更。参考的实例如下:
//推荐使用static final线程本地变量
private static final ThreadLocal<Foo> LOCAL_FOO = new ThreadLocal<Foo>();
以上代码,为什么ThreadLocal实例除了添加static final 修饰之后,还常常加上了 private
修饰呢?
主要目的是 缩小使用的范围,尽可能不让他人引用。
凡事都有两面性,使用static 、final修饰ThreadLocal实例也会带来副作用: 内存泄漏。
-
为啥内存泄露又出来了?
-
上面不是解决了吗?
-
呜呜呜 !!!
由于使用static 、final修饰TheadLocal对象实例, 导致了咱们这个被 ThreadLocalMap中Entry的Key所引用的ThreadLocal对象实例,一直存在强引用。
这里有一个严重后果,这个 使用static 、final修饰TheadLocal对象实例 一直不会被GC,一直存在,一直存在。
TheadLocal对象实例存在强引用,会导致三个彻底失效:
导致JDK解决key内存泄露问题的弱引用清理方式彻底失效。
导致JDK解决value内存泄露问题的两大清理方式彻底失效。
- 探测式清理(Proactive Cleanup) 彻底失效
- 启发式清理(Heuristic Cleanup)彻底失效 。
这使得Thread实例内部的ThreadLocalMap中Entry的Key在Thread实例的生命期内将始终保持为非null,从而导致Key所在的Entry不会被自动清空,这就会导致Entry中的Value指向的对象一直存在强引用,Value指向的对象在线程生命期内不会被释放,最终导致内存泄露。
所以,使用static 、final修饰TheadLocal实例,使用完后必须使用remove()
进行手动释放。
如果使用线程池,可以定制线程池的afterExecute
方法(任务执行完成之后的钩子方法),在任务执行完成之后,调用TheadLocal实例的remove()方法
对其手动释放,从而实现的其线程内部的Entry得到释放,参考的代码如下:
//线程本地变量,用于记录线程异步任务的开始执行时间
private static final ThreadLocal<Long> START_TIME= new ThreadLocal<>();
ExecutorService pool = new ThreadPoolExecutor(2,
4, 60,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(2)) {
...省略其他
//异步任务执行完成之后的钩子方法
@Override
protected void afterExecute(Runnable target, Throwable t)
{
...省略其他
//清空TheadLocal实例的本地值
startTime.remove();
}
};
ThreadLocal升级版1:InheritableThreadLocal 可继承本地变量
什么是可继承本地变量InheritableThreadLocal(ITL)?
文档太长,超过了 平台限制…
这部分详细内容略,请参见PDF 《ThreadLocal 学习圣经:一次穿透TL、ITL、TTL、FTL》
InheritableThreadLocal的基本使用
文档太长,超过了 平台限制…
这部分详细内容略,请参见PDF 《ThreadLocal 学习圣经:一次穿透TL、ITL、TTL、FTL》
InheritableThreaLocal的原理分析
文档太长,超过了 平台限制…
这部分详细内容略,请参见PDF 《ThreadLocal 学习圣经:一次穿透TL、ITL、TTL、FTL》
InheritableThreaLocal所带来的问题
文档太长,超过了 平台限制…
这部分详细内容略,请参见PDF 《ThreadLocal 学习圣经:一次穿透TL、ITL、TTL、FTL》
ThreadLocal升级版2:TransmittableThreadLocal 可透传本地变量
文档太长,超过了 平台限制…
这部分详细内容略,请参见PDF 《ThreadLocal 学习圣经:一次穿透TL、ITL、TTL、FTL》
什么是TransmittableThreadLocal(TTL)?
文档太长,超过了 平台限制…
这部分详细内容略,请参见PDF 《ThreadLocal 学习圣经:一次穿透TL、ITL、TTL、FTL》
TTL 使用场景
文档太长,超过了 平台限制…
这部分详细内容略,请参见PDF 《ThreadLocal 学习圣经:一次穿透TL、ITL、TTL、FTL》
TransmittableThreadLocal的原理分析
文档太长,超过了 平台限制…
这部分详细内容略,请参见PDF 《ThreadLocal 学习圣经:一次穿透TL、ITL、TTL、FTL》
ThreadLocal、InheritableThreaLocal与TransmittableThreadLocal的比较
ThreadLocal、InheritableThreadLocal与TransmittableThreadLocal在Java中都是用于处理线程局部变量的工具,但它们在使用场景和特性上有所不同。
-
ThreadLocal
- 定义:ThreadLocal是Java中一个非常重要的线程技术,它为每个线程提供了它自己的变量副本,使得线程间无法相互访问对方的变量,从而避免了线程间的竞争和数据泄露问题。
- 特性:
- 使用场景:适用于需要在线程内部存储和获取数据,且不希望与其他线程共享数据的场景。
- 变量存储:提供了一种在线程内部存储变量的机制,每个线程可以独立地改变自己的副本,而不会影响到其他线程。
- 线程隔离:通过为每个线程创建变量副本,ThreadLocal实现了线程间的数据隔离,提高了多线程程序的性能。
- 变量连续性:当线程切换时,ThreadLocal可以保持变量的连续性。
-
InheritableThreadLocal
- 定义:InheritableThreadLocal是ThreadLocal的一个子类,它包含了ThreadLocal的所有功能,并扩展了ThreadLocal的功能。
- 特性:
- 线程继承:允许父线程中的InheritableThreadLocal变量的值被子线程继承。当创建一个新的线程时,这个新线程可以访问其父线程中InheritableThreadLocal变量的值。
- 使用场景:适用于需要在父线程和子线程之间传递数据的场景,如线程池中的任务传递等。
-
TransmittableThreadLocal
- 定义:TransmittableThreadLocal是阿里巴巴开源的一个框架,用于解决在使用线程池等场景下,ThreadLocal变量无法跨线程传递的问题。
- 特性:
- 跨线程传递:能够在多线程传递中保持变量的传递性,确保在父线程和子线程之间正确传递ThreadLocal变量。
- copy 方法用于定制 任务提交给线程池时 的 ThreadLocal 值传递到 任务执行时 的拷贝行为,缺省传递的是引用。注意:如果跨线程传递了对象引用因为不再有线程封闭,与 InheritableThreadLocal.childValue 一样,使用者/业务逻辑要注意传递对象的线程安全。
- protected 的 beforeExecute/afterExecute 方法执行任务(Runnable/Callable)的前/后的生命周期回调。
- 使用场景:适用于需要在线程池等场景下跨线程传递ThreadLocal变量的场景。
总结起来如下:
- ThreadLocal适用于线程内部的数据存储和访问,确保数据在线程间的隔离。
- InheritableThreadLocal适用于需要在父线程和子线程间传递数据的场景,实现数据的继承。
- TransmittableThreadLocal则是为了解决在使用线程池等场景下,ThreadLocal变量无法跨线程传递的问题,实现数据的跨线程传递。
在选择使用哪个类时,应根据具体的业务场景和需求进行权衡。同时,也需要注意在使用完这些类后,及时清理不再需要的数据,避免内存泄漏。
ThreadLocal和synchronized之间的比较
ThreadLocal和synchronized在Java中都是用于处理多线程问题的机制,但它们之间存在一些关键的区别。
-
核心思想
- ThreadLocal:其核心思想是以空间换时间。它为每个线程提供了一个独立的变量副本,使得每个线程都可以访问和修改自己的变量副本,而不会影响到其他线程。由于每个线程操作的是自己的变量副本,因此多个线程可以同时访问该变量,且相互之间不会产生影响。这种机制主要用于保存线程私有数据、提高性能、管理线程特定的资源等场景。
- synchronized:其核心思想是以时间换空间。它确保同一时刻只有一个线程能够执行被synchronized修饰的代码块或方法,其他线程必须等待锁的释放。多个线程访问的是同一个变量,当多个线程同时访问该变量时,需要抢占锁,并且等待获取锁的线程释放锁,因此会消耗较多的时间。synchronized主要用于保护共享资源,防止竞态条件和数据不一致问题。
-
应用场景
- ThreadLocal主要用于线程间的数据隔离,常见应用场景包括线程封闭(将对象封闭在单个线程中,避免线程安全问题)、保存线程上下文信息(如在Web开发中存储用户信息和请求参数)、数据库连接管理(确保每个线程获取到自己的数据库连接)以及线程池中的任务隔离等。
- Synchronized主要用于线程间的数据共享,常用于保护共享资源,如共享数据或对象,确保同一时间只有一个线程访问这些资源。此外,它还可以用于保护需要原子性执行的代码块,防止多线程并发执行导致的问题。
-
性能和资源消耗
- ThreadLocal为每个线程创建变量副本,因此会消耗较多的内存。但由于线程间互不干扰,所以并发性能较高。
- synchronized则通过锁机制来控制线程对共享资源的访问,虽然节省了内存空间,但在多线程环境下可能会因为锁竞争而降低性能。
综上所述:
ThreadLocal和synchronized在解决多线程访问相同变量的冲突问题上各有其特点和适用场景。选择使用哪种机制应根据具体的业务需求、性能要求和资源限制来决定。
FastThreadLocal (FTL)的实现原理
ThreadLocal
是一个常用的工具类,它允许我们创建线程局部变量。这意味着每个线程都可以独立地改变自己的副本,而不会影响其他线程所持有的数据。
然而,ThreadLocal
在高并发环境下存在一些问题:
- 内存占用:每个
ThreadLocal
变量都会在每个线程中持有一个独立的副本,这可能导致大量的内存占用。 - 性能开销:创建和销毁这些线程局部变量会带来额外的性能开销。
Netty 是一个追求极致高性能的组件, Netty 的 FastThreadLocal
就是为了解决这些问题而诞生的。
什么是FastThreadLocal (FTL)?
文档太长,超过了 平台限制…
这部分详细内容略,请参见PDF 《ThreadLocal 学习圣经:一次穿透TL、ITL、TTL、FTL》
FastThreadLocal 如何使用
`
文档太长,超过了 平台限制…
这部分详细内容略,请参见PDF 《ThreadLocal 学习圣经:一次穿透TL、ITL、TTL、FTL》
FastThreadLocal 的优势
文档太长,超过了 平台限制…
这部分详细内容略,请参见PDF 《ThreadLocal 学习圣经:一次穿透TL、ITL、TTL、FTL》
FastThreadLocal 为什么快
FastThreadLocal 的实现与 ThreadLocal 非常类似,Netty 为 FastThreadLocal 量身打造了
FastThreadLocalThread 和 InternalThreadLocalMap 两个重要的类。
下面我们看下这两个类是如何实现的。
FastThreadLocalThread 是对 Thread 类的一层包装,每个线程对应一个 InternalThreadLocalMap 实
例。
只有 FastThreadLocal 和 FastThreadLocalThread 组合使用时,才能发挥 FastThreadLocal 的性
能优势。
首先看下 FastThreadLocalThread 的源码定义:
public class FastThreadLocalThread extends Thread {
private InternalThreadLocalMap threadLocalMap;
// 省略其他代码
}
可以看出 FastThreadLocalThread 主要扩展了 InternalThreadLocalMap 字段,
FastThreadLocalThread 主要使用 InternalThreadLocalMap 存储数据
注意, FastThreadLocalThread 不再是使用 Thread 中的ThreadLocalMap。
所以想知道 FastThreadLocalThread 高性能的奥秘,必须要了解InternalThreadLocalMap 的设计原理。
上文中我们讲到了 ThreadLocal 的一个重要缺点,就是 ThreadLocalMap 采用线性探测法解决 Hash冲突性能较慢,那么 InternalThreadLocalMap 又是如何优化的呢?
首先一起看下 InternalThreadLocalMap 的内部构造。
public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap {
private static final InternalLogger logger = InternalLoggerFactory.getInstance(InternalThreadLocalMap.class);
private static final ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap =
new ThreadLocal<InternalThreadLocalMap>();
private static final AtomicInteger nextIndex = new AtomicInteger();
private static final int DEFAULT_ARRAY_LIST_INITIAL_CAPACITY = 8;
private static final int ARRAY_LIST_CAPACITY_EXPAND_THRESHOLD = 1 << 30;
// Reference: https://hg.openjdk.java.net/jdk8/jdk8/jdk/file/tip/src/share/classes/java/util/ArrayList.java#l229
private static final int ARRAY_LIST_CAPACITY_MAX_SIZE = Integer.MAX_VALUE - 8;
private static final int STRING_BUILDER_INITIAL_SIZE;
private static final int STRING_BUILDER_MAX_SIZE;
private static final int HANDLER_SHARABLE_CACHE_INITIAL_CAPACITY = 4;
private static final int INDEXED_VARIABLE_TABLE_INITIAL_SIZE = 32;
public static final Object UNSET = new Object();
/** Used by {@link FastThreadLocal} */
private Object[] indexedVariables;
// Core thread-locals
private int futureListenerStackDepth;
private int localChannelReaderStackDepth;
private Map<Class<?>, Boolean> handlerSharableCache;
private IntegerHolder counterHashCode;
private ThreadLocalRandom random;
private Map<Class<?>, TypeParameterMatcher> typeParameterMatcherGetCache;
private Map<Class<?>, Map<String, TypeParameterMatcher>> typeParameterMatcherFindCache;
// String-related thread-locals
private StringBuilder stringBuilder;
private Map<Charset, CharsetEncoder> charsetEncoderCache;
private Map<Charset, CharsetDecoder> charsetDecoderCache;
// ArrayList-related thread-locals
private ArrayList<Object> arrayList;
private BitSet cleanerFlags;
/** @deprecated These padding fields will be removed in the future. */
public long rp1, rp2, rp3, rp4, rp5, rp6, rp7, rp8;
static {
STRING_BUILDER_INITIAL_SIZE =
SystemPropertyUtil.getInt("io.netty.threadLocalMap.stringBuilder.initialSize", 1024);
logger.debug("-Dio.netty.threadLocalMap.stringBuilder.initialSize: {}", STRING_BUILDER_INITIAL_SIZE);
STRING_BUILDER_MAX_SIZE = SystemPropertyUtil.getInt("io.netty.threadLocalMap.stringBuilder.maxSize", 1024 * 4);
logger.debug("-Dio.netty.threadLocalMap.stringBuil
der.maxSize: {}", STRING_BUILDER_MAX_SIZE);
}
// 省略其他代码
}
从 InternalThreadLocalMap 内部实现来看,与 ThreadLocalMap 一样都是采用数组
的存储方式。
但是InternalThreadLocalMap 并没有使用线性探测法来解决 Hash 冲突,而是另辟蹊径,使用数组替代map。
简单来说,而是在 FastThreadLocal 初始化 的时候,为每一个本地变量,分配一个全局唯一的索引 index
,数组索引 index 的值采用原子类 AtomicInteger
保证顺序递增,
-
每一个本地变量,分配一个
全局唯一的索引 index
. -
这个数组索引 index 的值 和本地变量绑定, 通过调用InternalThreadLocalMap.nextVariableIndex() 方法获得。
然后在读写数据的时候通过数组下标index直接定位到 FastThreadLocal 的位置,时间复杂度为 O(1)。
和 普通的ThreadLocalMap 相比, InternalThreadLocalMap 的 大致内部结构,如下:
假设现在我们有一批数据需要添加到数组中,分别为 value1、value2、value3、value4,对应的
FastThreadLocal 在初始化的时候生成的数组索引分别为 1、2、3、4。如下图所示:
如果数组下标递增到非常大,那么数组也会比较大,所以 FastThreadLocal 是通过空间换时间的思想提升读写性能。
完整的 FastThreadLocal结构分析,下面通过一幅具体的图,描述InternalThreadLocalMap、index 和 FastThreadLocal 之间的关系。
-
InternalThreadLocalMap中并不是Entry的key-value结构, 而是Object数组
-
索引0位置存放FastThreadLocal的Set集合, 其他索引位置初始化为UNSET, 数据存入的时候更新为具体的Object
-
FastThreadLocal中包含一个自增的index表示在InternalThreadLocalMap的数组中的索引位置
-
Set<FastThreadLocal<?>>结构中存放FastThreadLocal的引用, 更容易解决内存泄漏的问题
通过上面 FastThreadLocal 的内部结构图,我们对比下与 ThreadLocal 有哪些区别?
-
FastThreadLocal 使用 Object 数组替代了 Entry 数组,
-
Object[0] 存储的是一个Set集合,
-
从数组下标 1 开始都是直接存储的 value 数据,不再采用ThreadLocal 的键值对形式进行存储。
FastThreadLocal 源码分析
文档太长,超过了 平台限制…
这部分详细内容略,请参见PDF 《ThreadLocal 学习圣经:一次穿透TL、ITL、TTL、FTL》
FastThreadLocal 的回收机制
文档太长,超过了 平台限制…
这部分详细内容略,请参见PDF 《ThreadLocal 学习圣经:一次穿透TL、ITL、TTL、FTL》
FastThreadLocal 在Netty中的应用
文档太长,超过了 平台限制…
这部分详细内容略,请参见PDF 《ThreadLocal 学习圣经:一次穿透TL、ITL、TTL、FTL》
和ThreadLocal相比, FastThreadLocal 的优势:
文档太长,超过了 平台限制…
这部分详细内容略,请参见PDF 《ThreadLocal 学习圣经:一次穿透TL、ITL、TTL、FTL》
说在最后:有问题找老架构取经
ThreadLocal 相关的面试题,是非常常见的面试题。
以上的内容,如果大家能对答如流,如数家珍,基本上 面试官会被你 震惊到、吸引到。最终,让面试官爱到 “不能自已、口水直流”。offer, 也就来了。
在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典》V174,在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。
另外,如果没有面试机会,可以找尼恩来帮扶、领路。尼恩已经指导了大量的就业困难的小伙伴上岸.
前段时间,帮助一个40岁+就业困难小伙伴拿到了一个年薪100W的offer,小伙伴实现了 逆天改命 。
尼恩技术圣经系列PDF
- 《NIO圣经:一次穿透NIO、Selector、Epoll底层原理》
- 《Docker圣经:大白话说Docker底层原理,6W字实现Docker自由》
- 《K8S学习圣经:大白话说K8S底层原理,14W字实现K8S自由》
- 《SpringCloud Alibaba 学习圣经,10万字实现SpringCloud 自由》
- 《大数据HBase学习圣经:一本书实现HBase学习自由》
- 《大数据Flink学习圣经:一本书实现大数据Flink自由》
- 《响应式圣经:10W字,实现Spring响应式编程自由》
- 《Go学习圣经:Go语言实现高并发CRUD业务开发》
……完整版尼恩技术圣经PDF集群,请找尼恩领取
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓