Java并发编程(十)-ThreadLocal
参考文献
- 【对线面试官】ThreadLocal
- JAVA并发编程实战
- Spring 4.3: Using a TaskDecorator to copy MDC data to @Async threads
- 从ThreadLocal到InheritThreadLocal再到阿里的TransmittableThreadLocal,吊打面试官
ThreadLocal
它运行你将每个线程与持有数值的对象关联在一起.
ThreadLocal
提供了get
与set
访问器,为每个使用它的线程维护一份单独的拷贝.所以get
总是返回由当前执行线程通过set
设置的最新值.
- 一种解决多线程环境下成员变量的问题的方案,但是与线程同步无关.其思路是为每一个线程创建一个单独的变量副本,从而每个线程都可以独立地改变自己所拥有的变量副本,而不会影响其他线程所对应的副本
-
线程本地(
ThreadLocal
)变量通常用与基于可变的单体(Singleton)或全局变量的设计中,出现(不正确的)共享.比如说,一个单线程的应用程序可能会维护一个全局的数据连接,这个
Connection
在启动时就已经被初始化了.这样就可以避免为每个方法都传递一个Connection
.因为JDBC规范并未要求Connection
自身一定是线程安全的.通过利用ThreadLocal
存储JDBC连接,每个线程都会拥有属于自己的Connection
.1
2
3
4
5
6
7
8
9private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
}
public static Connection getConnection() {
return connectionHolder.get();
} -
注意:
ThreadLocal
不是用于解决共享变量的问题的,也不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制. -
ThreadLocal
的实现原理,每一个Thread
维护一个ThreadLocalMap
(线程本地变量,线程私有,K-V
型),key
为使用弱引用的ThreadLocal
实例,value
为线程变量的副本. -
四个方法
get()
:返回此线程局部变量的当前线程副本中的值initialValue()
:返回此线程局部变量的当前线程的“初始值”remove()
:移除此线程局部变量当前线程的值set(T value)
:将此线程局部变量的当前线程副本中的值设置为指定值
ThreadLocalMap
-
实现线程隔离机制的关键
-
每个
Thread
内部都有一个ThreadLocal.ThreadLocalMap
类型的成员变量,该成员变量用来存储实际的ThreadLocal
变量副本. -
提供了一种用键值对方式存储每一个线程的变量副本的方法,
key
为当前ThreadLocal
对象,value
则是对应线程的变量副本 -
注意点
ThreadLocal
实例本身是不存储值,它只是提供了一个在当前线程中找到副本值得key- 是
ThreadLocal
包含在Thread
中,而不是Thread
包含在ThreadLocal
中
-
ThreadLocalMap
使用ThreadLocal
的弱引用作为key
,如果一个ThreadLocal
不存在外部强引用时,Key
(ThreadLocal
)势必会被GC
回收,这样就会导致ThreadLocalMap
中key
为null
, 而value
还存在着强引用,只有Thead
线程退出以后,value
的强引用链条才会断掉,但如果当前线程再迟迟不结束的话,这些key
为null
的Entry
的value
就会一直存在一条强引用链.
内存泄漏问题
key 使用强引用
- 当
ThreadLocalMap
的key
为强引用回收ThreadLocal
时,因为ThreadLocalMap
还持有ThreadLocal
的强引用,如果没有手动删除,ThreadLocal
不会被回收,导致Entry内存泄漏.
key 使用弱引用
- 当
ThreadLocalMap
的key
为弱引用回收ThreadLocal
时,由于ThreadLocalMap
持有ThreadLocal
的弱引用,即使没有手动删除,ThreadLocal
也会被回收.当key
为null
,在下一次ThreadLocalMap
调用remove()
方法的时候会被清除value
值.ThreadLocal
内存泄漏的根源是:由于ThreadLocalMap
的生命周期跟Thread
一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用.
ThreadLocal
正确的使用方法
- 每次使用完
ThreadLocal
都调用它的remove()
方法清除数据 - 将
ThreadLocal
变量定义成private static
,这样就一直存在ThreadLocal
的强引用,也就能保证任何时候都能通过ThreadLocal
的弱引用访问到Entry
的value
值,进而清除掉 .
FastThreadLocal
FastThreadLocal
适用于高并发场景下需要频繁使用ThreadLocal
对象的情况.与传统的ThreadLocal
相比,FastThreadLocal
具有更高的访问速度和更小的内存占用.
使用场景
- 高并发场景下频繁使用
ThreadLocal
对象的情况; - 对性能有较高要求的场景,通过使用
FastThreadLocal
可以提高访问速度,减少竞争; - 大规模系统中需要访问
ThreadLocal
对象,通过使用FastThreadLocal
可以减少内存占用.
注意点
FastThreadLocal
的 API 接口与ThreadLocal
相似,但是并不兼容;FastThreadLocal
创建的对象不能被其他线程所共享;FastThreadLocal
对象的初始化可以在 get 方法中完成,这样可以减少对象创建的数量;- 由于
FastThreadLocal
对象并不是被线程本地引用的,因此需要手动释放对象的资源,否则可能会导致内存泄漏; FastThreadLocal
对象的数量需要谨慎控制,过多的对象会占用过多的内存,并且会增加垃圾回收的复杂度.FastThreadLocal
只能在FastThreadLocalThread线程中使用,这是因为
FastThreadLocalThread是由
FastThreadLocal`创建的一种线程类型,它可以提供更快速的线程局部变量访问和更少的竞争.- 在标准Thread线程中使用
FastThreadLocal
不是明智之举,因为FastThreadLocalThread是在FastThreadLocal
类的设计中存在的,同时FastThreadLocalThread还提供了一些其他相关的功能.在标准Thread线程中使用FastThreadLocal
可能会导致性能瓶颈,并且在代码中也容易引起混淆和不必要的麻烦.
- 在标准Thread线程中使用
1 | package cn.holelin.netty; |
1 | package cn.holelin.netty; |
ThreadLocal
,InheritThreadLocal
,阿里的TransmittableThreadLocal
ThreadLocal
可以用来解决什么问题,你工作中或者使用的框架中哪里用到了?ThreadLocal
主要用于解决多线程环境下共享变量的问题,它可以为每个线程创建一个独立的变量副本,使得每个线程都可以独立地操作自己的变量副本而不会相互干扰.- 使用
ThreadLocal
来管理用户请求的上下文信息,比如用户的身份认证信息、请求的语言环境等.这样可以确保在多个线程同时处理不同用户请求时,上下文信息不会混乱.
ThreadLocal
解决不了什么问题?怎么办ThreadLocal
无法解决跨线程传递变量的问题,即如果在一个线程中创建了一个新的线程,那么无法直接将ThreadLocal
中的值传递给子线程.要解决这个问题,可以使用InheritableThreadLocal
.InheritableThreadLocal
是ThreadLocal
的子类,它可以实现将父线程中的ThreadLocal
值传递给子线程.通过InheritableThreadLocal
,可以在父子线程之间传递上下文信息.
- InheritThreadLocal解决不了什么问题,怎么办
- 虽然
InheritableThreadLocal
可以解决父子线程之间的传递问题,但无法解决线程池中重用线程的情况.当线程池中的线程被重复使用时,InheritableThreadLocal
无法保证每次重用的线程都能正确地继承父线程的ThreadLocal
值.- 要解决这个问题,可以使用
TransmittableThreadLocal
. Spring
的解决方式: https://moelholm.com/blog/2017/07/24/spring-43-using-a-taskdecorator-to-copy-mdc-data-to-async-threads
- 要解决这个问题,可以使用
- 虽然
- 阿里的
TransmittableThreadLocal
是如何解决这个问题的- 阿里的
TransmittableThreadLocal
通过字节码增强的方式,修改了线程池的代码,使得在线程池创建线程时,对Runnable
或Callable
进行包装,并将需要传递的ThreadLocal
值存储到包装类中.当线程执行包装类中的逻辑时,首先将包装类中保存的ThreadLocal
值重新放入ThreadLocal
中,然后再执行真正的业务逻辑.这样就保证了在线程池中正确传递ThreadLocal
的值,解决了InheritableThreadLocal
无法处理线程池重用线程的问题.
- 阿里的