通俗易懂讲解ThreadLocal,不懂的过来打我

本文通过比喻解释ThreadLocal在单线程和多线程中的作用,将其比作孙悟空的分身,强调其线程隔离特性。讨论了ThreadLocal的使用场景和弊端,如降低代码耦合性和影响程序性能。并通过源码分析,揭示ThreadLocal、ThreadLocalMap与Thread的关系,以及可能引发的内存泄漏问题。

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

先举个例子,ThreadLocal是啥呢?在一个线程的情况下,threadlocal就相当于一个仓库,很简单,就是一个保存值的局部变量,通过set get 方法来从这个仓库中存储和获取。 但是在多个线程的情况下,就很不一样了,threadlocal就变成了孙悟空,对,你没看错,变成了孙悟空,为啥是孙悟空呢?因为孙悟空会分身术,会将自己变成多个孙悟空,而且变出来的每个孙悟空都各自具有各自的思想,是完全独立的,这个多个孙悟空之间不存在任何的联系。有多少个线程就有多少个孙悟空,每个线程里的孙悟空虽然名字都叫做孙悟空,但是他们之间没有任何的联系!!!

在这里插入图片描述在这里插入图片描述在这里插入图片描述

代码演示

在这里插入图片描述在这里插入图片描述
此时content局部变量还没有用到threadlocal
则打印的结果可能会发生错乱,如下图所示
在这里插入图片描述在这里插入图片描述

再次打印,结果就不会发生错乱了
在这里插入图片描述

重点来啦

在这里插入图片描述

synchronized是只有一份资源,多个人抢着来用,因此加锁,排队!比如多个人要去上厕所,但只有一个坑,因此每次只能一个人去上,上的时候怕别人乱闯进来,因此要加把锁
threadlocal是复制多个副本,供多个人一起使用。现在就是有多少个人,就有多少个坑了,可以都同时上厕所

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

这种方法的弊端 1、降低代码耦合性,参数传递使得service层和dao层之间有了直接的联系 2、降低程序性能,通过synchronized关键字使得程序失去了并发性,只能一个线程一个线程的去执行

在这里插入图片描述在这里插入图片描述

总结一下:1、传递数据,相当于仓库 2、线程隔离,因为它是孙悟空!!

ThrealLocal原理

下面那个图不知道有没有看懂,那一个一个的格子,就相当于一个一个的key-value,那一个一个的格子组成的长条,就是一个map

通俗的讲,以前的那种设计是 以ThreadLocal为基准的,也就是上面说的仓库,其实这里理解为仓库更为准确,因为无论多少个线程都只有一个Threadlocal,所以相当于一个仓库,虽然是多线程,但是他没有变成孙悟空,还是勤勤恳恳的做一个仓库,以这个仓库为老大,一切以
仓库着重考虑,所以每创建一个线程,仓库里面的Map(注意这里的Map 刚开始是没有Map的,是第一次使用set的时候创建的)就多一个entry键值对。。。注意这里,无论有多少个线程,都只有一个Map, 以新创建的thread为key,以存入的数为value,反正Threadlocal是大佬级别的,一切以Threadlocal为主

而现在呢?一切以Thread为主,现在是Thread为大佬,每创建一个线程,线程都会创建一个Map,Map的key存储Threadlocal,也就是你上面说的孙悟空,多个线程就有多个Threadlocal,因为多个线程就有多个Map,而Map的key就是这个Threadlocal

在这里插入图片描述在这里插入图片描述在这里插入图片描述

set源码解读

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

说明一个ThreadLocal 和 ThreadLocalMap 和 Thread 的区别,看源码的时候,好多人都被几个玩意儿弄混了,现在说明一下:

ThreadLocal是一个类

在这里插入图片描述
ThreadLocalMap类是ThreadLocal的内部类,set和get方法是ThreadLocal的成员方法,所以在刚开始没有Map的时候,是可以直接在set方法里面创建的,但是之后需要用到Map的时候,Map是在Thread类里面的成员变量,Thread类里面的成员变量threadlocals的类型就是
ThreadLocalMap,所以直接通过Thread t 的 t.threadlocals就得到了这个ThreadLocalMap

ThreadLocalMap源码分析

threadLocalMap源码是最复杂的,我们从3个方面看下
在这里插入图片描述在这里插入图片描述

table是一个数组,里面元素类型是Entry类型
而Entry是一个类,存储的是键值对

在这里插入图片描述

强引用不会被回收,弱引用会被回收

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述
这里需要注意的是,有两点 。 第一点: threadlocal定义的时候是以局部变量的方式定义,所以threadlocal类型的变量引用是保存在栈中。 第二点:每一个线程都有自己的threadlocalMap,而当前线程的这个ThreadlocalMap的key就是threadlocal,所以就有threadRef->currentThread->threadLocalMap->entry

Entry中的threadLocal是弱引用的时候,这时候当ThreadLocal使用完毕的时候, ThreadLocal Ref 因为是在栈中,所以自然被退出栈了,ThreadLocal退栈之后,因为threadlocal是弱引用,就会使得ThreadLocal被垃圾回收,threadlocal回收之后,就相当于没有了key,这时候这个Entry的Key就没有了,就是null了

最关键的是还有一个当前正在运行的线程指向的Map,map里面的value不会被回收,所以也有可能是内存泄漏在这里插入图片描述

重点知识点:弱引用会使得key为null ,这样下一次set() 和 get() 时,就会使得value也为null ,即value在下一次map调用set,get方法时会被清除,从而有效的避免了内存泄漏

既然用到了Map结构,就一定会有hash冲突的问题,因为Map结构其实就是数组+链表(红黑树),数组里面存放的就是

Entry(键值对),那该怎么存放呢?键值对该数组的哪个位置呢?这里就涉及到了hash冲突
在这里插入图片描述

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

<think>嗯,用户现在想让我用通俗的话讲解一下ThreadLocal的原理。之前他们问过令牌桶算法和信号量的关系,可能对并发编程比较感兴趣。ThreadLocal也是一个和线程相关的概念,但可能他们不太清楚具体是怎么工作的。 首先,我需要确保自己正确理解ThreadLocal的原理。ThreadLocal是用来在每个线程中保存独立的变量副本,这样每个线程访问自己的变量,不会和其他线程冲突。这通常用于线程安全的场景,比如数据库连接或者用户会话信息,每个线程需要自己的实例。 用户可能已经知道多线程的基本概念,但不太明白ThreadLocal如何实现线程隔离。我需要用通俗的比喻来解释,比如每个线程有自己的储物柜,存放自己的东西,这样就不会拿错别人的东西。储物柜的比喻可能比较贴切,容易理解。 然后要分解ThreadLocal的关键点:如何存储数据。每个Thread对象内部有一个ThreadLocalMap,这个Map的key是ThreadLocal实例本身,value是存储的值。当调用set或get方法时,实际上是在当前线程的Map里操作。这样不同线程访问同一个ThreadLocal对象时,实际上是在各自线程的Map里操作,所以数据互不影响。 可能用户会疑惑为什么ThreadLocalMap的key是弱引用。这里需要解释内存泄漏的问题,如果key是强引用,即使ThreadLocal不再使用,由于线程存活,Map里的Entry也不会被回收,导致内存泄漏。弱引用可以让GC在回收ThreadLocal实例后,key变为null,虽然Entry还在,但后续访问时会清理掉这些无效的Entry。不过最好建议在使用完后调用remove方法,避免潜在的内存泄漏。 另外,用户可能不知道ThreadLocal的实际应用场景,可以举例子,比如在Web开发中保存用户登录信息,每个请求线程独立,或者数据库连接管理。这样用户能更好地理解为什么需要ThreadLocal。 需要避免使用专业术语,保持口语化。比如,不要说“线程本地存储”,而是说“每个线程自己独有的存储空间”。检查是否有复杂的概念需要进一步简化,确保解释清晰易懂。 可能用户之前接触过线程安全的问题,比如用synchronized或者Lock,而ThreadLocal提供了另一种避免竞争的方法,即每个线程有自己的副本,不需要同步。这也需要强调ThreadLocal的适用场景,适合变量在线程间隔离,而不是共享的情况。 最后总结一下,确保用户能抓住重点:每个线程有自己的Map,ThreadLocal作为key存取数据,避免线程间竞争,使用时注意内存泄漏问题。</think>好的!我用一个生活中常见的比喻来解释 **ThreadLocal** 的原理,保证通俗易懂~ --- ### **1. 场景比喻:储物柜和钥匙** 假设你和小明、小红一起去健身房,健身房有一排储物柜(**ThreadLocal**)。规则是: - **每个人(线程)有一个专属钥匙(ThreadLocal对象)** - **钥匙只能开自己的储物柜,看不到别人的** - **储物柜里可以放自己的私人物品(变量)** 比如: - **你的钥匙(ThreadLocal)** ➜ 开你的柜子 ➜ 放你的水杯(变量)。 - **小明的钥匙(ThreadLocal)** ➜ 开他的柜子 ➜ 放他的毛巾(变量)。 - **互不干扰!** 你拿不到小明的毛巾,他也拿不到你的水杯。 --- ### **2. 原理拆解** - **每个线程(人)** 内部有一个“隐藏的背包”(**ThreadLocalMap**)。 - **ThreadLocal(钥匙)** 的作用是:从背包里找到自己的储物柜格子(Entry),存取数据。 #### **关键步骤**: 1. **存数据(set)**: 用你的钥匙(ThreadLocal对象)在背包里标记一个格子,存水杯(变量)。 ```java ThreadLocal<String> locker = new ThreadLocal<>(); locker.set("你的水杯"); // 存到当前线程的背包里 ``` 2. **取数据(get)**: 用同一把钥匙(ThreadLocal对象)从背包里找到自己的格子,取出水杯。 ```java String thing = locker.get(); // 从当前线程的背包取 ``` 3. **删数据(remove)**: 清理背包中自己钥匙对应的格子,防止占着不用(避免内存泄漏)。 ```java locker.remove(); ``` --- ### **3. 为什么能隔离不同线程?** - **背包是线程私有的**:每个线程的 `ThreadLocalMap` 是独立的,其他线程访问不到。 - **钥匙(ThreadLocal)只是“标记”作用**:同一把钥匙在不同线程中,打开的其实是各自背包里的不同格子。 比如: - 线程A用 `locker.set("A的水杯")` ➜ 存到线程A的背包。 - 线程B用 `locker.set("B的水杯")` ➜ 存到线程B的背包。 - 线程A和B调用 `locker.get()` 时,各自拿到的都是自己背包里的水杯。 --- ### **4. 内存泄漏问题(潜在风险)** - **钥匙不用了,但格子还在**:如果钥匙(ThreadLocal对象)被丢弃,但格子(Entry)还在背包里,可能占着内存不释放。 - **解决办法**:用完钥匙后,主动调用 `locker.remove()` 清理格子。 --- ### **5. 实际应用场景** - **数据库连接**:每个线程用自己的连接,避免多线程冲突。 - **用户登录信息**:Web请求中,每个线程保存当前用户的信息。 - **日期格式化**:SimpleDateFormat 非线程安全,用 ThreadLocal 为每个线程配一个独立的实例。 --- ### **一句话总结** **ThreadLocal 让每个线程自带“私有储物柜”,用同一把钥匙(ThreadLocal对象)存取,但实际开的是自己柜子,和其他人完全隔离!**
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值