java多线程 ThreadLocal理论总结

本文详细介绍了Java中的ThreadLocal,包括其用途、优点、原理和应用场景。ThreadLocal为每个线程提供了独立的变量副本,避免了线程安全问题。文章还探讨了线程池中使用ThreadLocal的注意事项,如需防止内存泄漏,应在使用完毕后调用remove方法。此外,文章解析了ThreadLocal的实现原理,包括数据结构、内存模型和哈希冲突处理,并分析了可能导致内存泄漏的原因。最后,文章讨论了ThreadLocalMap的set和get方法的工作流程以及内存管理策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

ThreadLocal 简介

ThreadLocal的用途

使用ThreadLocal有什么好处?(可以解决什么问题?)

什么是ThreadLocal变量

ThreadLocal 应用场景

线程池中的线程使用ThreadLocal需要注意什么?

使用样例

简单说下ThreadLocal的实现原理

ThreadLocal的数据结构

ThreadLocal内存模型原理

ThreadLocal哈希与魔数

ThreadLocalMap Hash 冲突

ThreadLocalMap

为啥要把ThreadLocal做为key,而不是Thread做为key?

为什么Entry继承了WeakReference?

Entry继承了WeakReference,可能造成内存泄漏?

ThreadLocal 内存泄漏,remove方法

Thread类的两个map

ThreadLocal 的 set 方法

ThreadLocalMap的 set 方法原理

ThreadLocalMap的set方法源码

ThreadLocalMap过期 key 的探测式清理流程

ThreadLocalMap 的扩容机制

ThreadLocal 的 get 方法

ThreadLocalMap的getEntry()方法

ThreadLocalMap过期 key 的启发式清理流程

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视作ThreadLocalvalue为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。

每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离

ThreadLocalMap有点类似HashMap的结构,只是HashMap是由数组+链表实现的,而ThreadLocalMap中并没有链表结构。

我们还要注意Entry, 它的keyThreadLocal<?> k ,继承自WeakReference, 也就是我们常说的弱引用类型。

ThreadLocal内存模型原理

图中左边是栈,右边是堆。线程的一些局部变量和引用使用的内存属于Stack(栈)区,而普通的对象是存储在Heap(堆)区。

线程运行时,我们定义的TheadLocal对象被初始化,存储在Heap,同时线程运行的栈区保存了指向该实例的引用,也就是图中的ThreadLocalRef。

当ThreadLocal的set/get被调用时,虚拟机会根据当前线程的引用也就是CurrentThreadRef找到其对应在堆区的实例,然后查看其对用的TheadLocalMap实例是否被创建,如果没有,则创建并初始化。

Map实例化之后,也就拿到了该ThreadLocalMap的句柄,那么就可以将当前ThreadLocal对象作为key,进行存取操作。

图中的虚线,表示key对应ThreadLocal实例的引用是个弱引用。

ThreadLocal哈希与魔数

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值