目录
使用ThreadLocal有什么好处?(可以解决什么问题?)
为啥要把ThreadLocal做为key,而不是Thread做为key?
Entry继承了WeakReference,可能造成内存泄漏?
InheritableThreadLocal与共享ThreadLocal
注意:本文参考了https://baijiahao.baidu.com/s?id=1663127810801876375
参考 万字解析 ThreadLocal 关键字 | JavaGuide
ThreadLocal 简介
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK 中提供的ThreadLocal
类正是为了解决这样的问题。 ThreadLocal
类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal
类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
如果你创建了一个ThreadLocal
变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal
变量名的由来。他们可以使用 get()
和 set()
方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。
再举个简单的例子:
比如有两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么 ThreadLocal 就是用来避免这两个线程竞争的。
ThreadLocal的用途
ThreadLocal用来给各个线程提供线程隔离的局部变量。使用很简单,通过调用同一个ThreadLocal对象的get/set方法来读写这个ThreadLocal对象对应的value,但是线程A set值后,不会影响到线程B之后get到的值。
ThreadLocal对象通常是static的,因为它在map里作为key使用,所以在各个线程中需要复用。
使用ThreadLocal有什么好处?(可以解决什么问题?)
相比synchronized使用锁从而使得多个线程可以安全的访问同一个共享变量,现在可以直接转换思路,让线程使用自己的私有变量,直接避免了并发访问的问题。
当一个数据需要传递给某个方法,而这个方法处于方法调用链的中间部分,那么如果采用加参数传递的方式,势必为影响到耦合性。而如果使用ThreadLocal来为线程保存这个数据,就避免了这个问题,而且对于这个线程,它在任何时候都可以取到这个值。
什么是ThreadLocal变量
ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:
因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
可以通过 ThreadLocal<T> value = new ThreadLocal<T>(); 来使用。
ThreadLocal 应用场景
ThreadLocal 的特性也导致了应用场景比较广泛,主要的应用场景如下:
线程间数据隔离,各线程的 ThreadLocal 互不影响
方便同一个线程使用某一对象,避免不必要的参数传递
当需要实现线程安全的变量时。
当需要减少线程资源竞争的时候。
全链路追踪中的 traceId 或者流程引擎中上下文的传递一般采用 ThreadLocal
管理Session会话,将Session保存在ThreadLocal中,使线程处理多次处理会话时始终是一个Session。
Spring 事务管理器采用了 ThreadLocal,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。Spring框架里面就是用的ThreadLocal来实现这种隔离,主要是在TransactionSynchronizationManager
这个类里面
private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName =
new NamedThreadLocal<>("Current transaction name");
……
Spring的事务主要是ThreadLocal和AOP去做实现的,我这里提一下,大家知道每个线程自己的链接是靠ThreadLocal保存的就好了
Spring MVC 的 RequestContextHolder 的实现使用了 ThreadLocal
线程池中的线程使用ThreadLocal需要注意什么?
由于ThreadLocalMap是Thread对象的成员,当对应线程运行结束销毁时,自然这个ThreadLocalMap类型的成员也会被回收。
但如果想依赖上面这点来避免内存泄漏,就大错特错了。因为线程池里的线程为了复用线程,一般不会直接销毁掉完成了任务的线程,以下一次复用。
所以,线程使用完毕ThreadLocal后,主动调用remove来避免内存泄漏,才是万全之策。
另外,线程池中的线程使用完毕ThreadLocal后,不主动调用remove,还可能造成:get值时,get到上一个任务set的值,直接造成程序错误。
使用样例
main线程和new Thread线程是2个线程,对应的threadlocal不同
首先在 Thread-0 线程执行之前,先给 THREAD_LOCAL 设置为 wupx,然后可以取到这个值,然后通过创建一个新的线程以后去取这个值,发现新线程取到的为 null,意外着这个变量在不同线程中取到的值是不同的,不同线程之间对于 ThreadLocal 会有对应的副本,接着在线程 Thread-0 中执行对 THREAD_LOCAL 的修改,将值改为 huxy,可以发现线程 Thread-0 获取的值变为了 huxy,主线程依然会读取到属于它的副本数据 wupx,这就是线程的封闭。
import java.text.SimpleDateFormat;
import java.util.Random;
public class ThreadLocalExample implements Runnable{
// SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));
public static void main(String[] args) throws InterruptedException {
ThreadLocalExample obj = new ThreadLocalExample();
for(int i=0 ; i<10; i++){
Thread t = new Thread(obj, ""+i);
Thread.sleep(new Random().nextInt(1000));
t.start();
}
}
@Override
public void run() {
System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
//formatter pattern is changed here by thread, but it won't reflect to other threads
formatter.set(new SimpleDateFormat());
System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
}
}
Output:
Thread Name= 0 default Formatter = yyyyMMdd HHmm
Thread Name= 0 formatter = yy-M-d ah:mm
Thread Name= 1 default Formatter = yyyyMMdd HHmm
Thread Name= 2 default Formatter = yyyyMMdd HHmm
Thread Name= 1 formatter = yy-M-d ah:mm
Thread Name= 3 default Formatter = yyyyMMdd HHmm
Thread Name= 2 formatter = yy-M-d ah:mm
Thread Name= 4 default Formatter = yyyyMMdd HHmm
Thread Name= 3 formatter = yy-M-d ah:mm
Thread Name= 4 formatter = yy-M-d ah:mm
Thread Name= 5 default Formatter = yyyyMMdd HHmm
Thread Name= 5 formatter = yy-M-d ah:mm
Thread Name= 6 default Formatter = yyyyMMdd HHmm
Thread Name= 6 formatter = yy-M-d ah:mm
Thread Name= 7 default Formatter = yyyyMMdd HHmm
Thread Name= 7 formatter = yy-M-d ah:mm
Thread Name= 8 default Formatter = yyyyMMdd HHmm
Thread Name= 9 default Formatter = yyyyMMdd HHmm
Thread Name= 8 formatter = yy-M-d ah:mm
Thread Name= 9 formatter = yy-M-d ah:mm
简单说下ThreadLocal的实现原理
每个线程在运行过程中都可以通过Thread.currentThread()获得与之对应的Thread对象,而每个Thread对象都有一个ThreadLocalMap类型的成员,ThreadLocalMap是一种hashmap,它以ThreadLocal作为key。
所以,只有通过Thread对象和ThreadLocal对象二者,才可以唯一确定到一个value上去。线程隔离的关键,正是因为这种对应关系用到了Thread对象。
线程可以根据自己的需要往ThreadLocalMap里增加键值对,当线程从来没有使用到ThreadLocal时(指调用get/set方法),Thread对象不会初始化这个ThreadLocalMap类型的成员。
ThreadLocal的数据结构
Thread
类有一个类型为ThreadLocal.ThreadLocalMap
的实例变量threadLocals
,也就是说每个线程有一个自己的ThreadLocalMap
。
ThreadLocalMap
有自己的独立实现,可以简单地将它的key
视作ThreadLocal
,value
为代码中放入的值(实际上key
并不是ThreadLocal
本身,而是它的一个弱引用)。
每个线程在往ThreadLocal
里放值的时候,都会往自己的ThreadLocalMap
里存,读也是以ThreadLocal
作为引用,在自己的map
里找对应的key
,从而实现了线程隔离。
ThreadLocalMap
有点类似HashMap
的结构,只是HashMap
是由数组+链表实现的,而ThreadLocalMap
中并没有链表结构。
我们还要注意Entry
, 它的key
是ThreadLocal<?> k
,继承自WeakReference
, 也就是我们常说的弱引用类型。
ThreadLocal
内存模型原理
图中左边是栈,右边是堆。线程的一些局部变量和引用使用的内存属于Stack(栈)区,而普通的对象是存储在Heap(堆)区。
线程运行时,我们定义的TheadLocal对象被初始化,存储在Heap,同时线程运行的栈区保存了指向该实例的引用,也就是图中的ThreadLocalRef。
当ThreadLocal的set/get被调用时,虚拟机会根据当前线程的引用也就是CurrentThreadRef找到其对应在堆区的实例,然后查看其对用的TheadLocalMap实例是否被创建,如果没有,则创建并初始化。
Map实例化之后,也就拿到了该ThreadLocalMap的句柄,那么就可以将当前ThreadLocal对象作为key,进行存取操作。
图中的虚线,表示key对应ThreadLocal实例的引用是个弱引用。
ThreadLocal哈希与魔数