news 2026/5/5 16:05:48

Java并发编程面试题:ThreadLocal(8题)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java并发编程面试题:ThreadLocal(8题)

🧑 博主简介:CSDN博客专家历代文学网(PC端可以访问:https://literature.sinhy.com/#/?__c=1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,精通Java编程高并发设计Springboot和微服务,熟悉LinuxESXI虚拟化以及云原生Docker和K8s,热衷于探索科技的边界,并将理论知识转化为实际应用。保持对新技术的好奇心,乐于分享所学,希望通过我的实践经历和见解,启发他人的创新思维。在这里,我希望能与志同道合的朋友交流探讨,共同进步,一起在技术的世界里不断学习成长。
技术合作请加本人wx(注明来自csdn):foreast_sea


Java并发编程面试题:ThreadLocal

1. ThreadLocal 是什么?

ThreadLocal是 Java 中提供的一种用于实现线程局部变量的工具类。它允许每个线程都拥有自己的独立副本,从而实现线程隔离,用于解决多线程中共享对象的线程安全问题。

在 Web 应用中,可以使用 ThreadLocal 存储用户会话信息,这样每个线程在处理用户请求时都能方便地访问当前用户的会话信息。

在数据库操作中,可以使用 ThreadLocal 存储数据库连接对象,每个线程有自己独立的数据库连接,从而避免了多线程竞争同一数据库连接的问题。

在格式化操作中,例如日期格式化,可以使用 ThreadLocal 存储 SimpleDateFormat 实例,避免多线程共享同一实例导致的线程安全问题。

使用 ThreadLocal 通常分为四步:

①、创建 ThreadLocal

//创建一个ThreadLocal变量publicstaticThreadLocal<String>localVariable=newThreadLocal<>();

②、设置 ThreadLocal 的值

//设置ThreadLocal变量的值localVariable.set("沉默王二是沙雕");

③、获取 ThreadLocal 的值

//获取ThreadLocal变量的值Stringvalue=localVariable.get();

④、删除 ThreadLocal 的值

//删除ThreadLocal变量的值localVariable.remove();

ThreadLocal 有哪些优点?

①、线程隔离:每个线程访问的变量副本都是独立的,避免了共享变量引起的线程安全问题。由于 ThreadLocal 实现了变量的线程独占,使得变量不需要同步处理,因此能够避免资源竞争。

②、数据传递方便:ThreadLocal 常用于在跨方法、跨类时传递上下文数据(如用户信息等),而不需要在方法间传递参数。

除了 ThreadLocal,还有什么解决线程安全问题的方法?

①、Java 中的 synchronized 关键字可以用于方法和代码块,确保同一时间只有一个线程可以执行特定的代码段。

publicsynchronizedvoidmethod(){// 线程安全的操作}

②、Java 并发包(java.util.concurrent.locks)中提供了 Lock 接口和一些实现类,如 ReentrantLock。相比于 synchronized,ReentrantLock 提供了公平锁和非公平锁。

ReentrantLocklock=newReentrantLock();publicvoidmethod(){lock.lock();try{// 线程安全的操作}finally{lock.unlock();}}

③、Java 并发包还提供了一组原子变量类(如 AtomicInteger,AtomicLong 等),它们利用 CAS(比较并交换),实现了无锁的原子操作,适用于简单的计数器场景。

AtomicIntegeratomicInteger=newAtomicInteger(0);publicvoidincrement(){atomicInteger.incrementAndGet();}

④、Java 并发包提供了一些线程安全的集合类,如 ConcurrentHashMap,CopyOnWriteArrayList 等。这些集合类内部实现了必要的同步策略,提供了更高效的并发访问。

ConcurrentHashMap<String,String>map=newConcurrentHashMap<>();

⑤、volatile 变量保证了变量的可见性,修改操作是立即同步到主存的,读操作从主存中读取。

privatevolatilebooleanflag=false;

2. 你在工作中用到过 ThreadLocal 吗?

有用到过,用来存储用户信息。

MVC 架构,登录后的用户每次访问接口,都会在请求头中携带一个 token,在控制层可以根据这个 token,解析出用户的基本信息。

假如在服务层和持久层也要用到用户信息,就可以在控制层拦截请求把用户信息存入 ThreadLocal。

这样我们在任何一个地方,都可以取出 ThreadLocal 中存的用户信息。

很多其它场景的 cookie、session 等等数据隔离都可以通过 ThreadLocal 去实现。

数据库连接池也可以用 ThreadLocal,将数据库连接池的连接交给 ThreadLocal 进行管理,能够保证当前线程的操作都是同一个 Connnection。

3. ThreadLocal 怎么实现的呢?

ThreadLocal 本身并不存储任何值,它只是作为一个映射,来映射线程的局部变量。当一个线程调用 ThreadLocal 的 set 或 get 方法时,实际上是访问线程自己的 ThreadLocal.ThreadLocalMap。

ThreadLocalMap 是 ThreadLocal 的静态内部类,它内部维护了一个 Entry 数组,key 是 ThreadLocal 对象,value 是线程的局部变量本身。

早期的 ThreadLocal 不是这样的,它的 ThreadLocalMap 中使用 Thread 作为 key,这也是最简单的实现方式。

优化后的方案有两个好处,一个是 Map 中存储的键值对变少了;另一个是 ThreadLocalMap 的生命周期和线程一样长,线程销毁的时候,ThreadLocalMap 也会被销毁。

Entry 继承了 WeakReference,它限定了 key 是一个弱引用,弱引用的好处是当内存不足时,JVM 会回收 ThreadLocal 对象,并且将其对应的 Entry 的 value 设置为 null,这样在很大程度上可以避免内存泄漏。

staticclassEntryextendsWeakReference<ThreadLocal<?>>{/** The value associated with this ThreadLocal. */Objectvalue;//节点类Entry(ThreadLocal<?>k,Objectv){//key赋值super(k);//value赋值value=v;}}

ThreadLocal 的实现原理就是,每个线程维护一个 Map,key 为 ThreadLocal 对象,value 为想要实现线程隔离的对象。

1、当需要存线程隔离的对象时,通过 ThreadLocal 的 set 方法将对象存入 Map 中。

2、当需要取线程隔离的对象时,通过 ThreadLocal 的 get 方法从 Map 中取出对象。

3、Map 的大小由 ThreadLocal 对象的多少决定。

什么是弱引用,什么是强引用?

强引用,比如说User user = new User("沉默王二")中,user 就是一个强引用,new User("沉默王二")就是一个强引用对象。

当 user 被置为 null 时(user = null),new User("沉默王二")将会被垃圾回收;如果 user 不被置为 null,即便是内存空间不足,JVM 也不会回收new User("沉默王二")这个强引用对象,宁愿抛出 OutOfMemoryError。

弱引用,比如说下面这段代码:

ThreadLocal<User>userThreadLocal=newThreadLocal<>();userThreadLocal.set(newUser("沉默王二"));

①、userThreadLocal 是一个强引用,new ThreadLocal<>()是一个强引用对象;

②、new User("沉默王二")是一个强引用对象。

③、在 ThreadLocalMap 中,key = new ThreadLocal<>()是一个弱引用对象。当 JVM 进行垃圾回收时,如果发现了弱引用对象,就会将其回收。

其关系链就是:

  • ThreadLocal 强引用 -> ThreadLocal 对象。
  • Thread 强引用 -> ThreadLocalMap。
  • ThreadLocalMap[i]强引用了 -> Entry。
  • Entry.key 弱引用 -> ThreadLocal 对象。
  • Entry.value 强引用 -> 线程的局部变量对象。

4. ThreadLocal 内存泄露是怎么回事?

通常情况下,随着线程 Thread 的结束,其内部的 ThreadLocalMap 也会被回收,从而避免了内存泄漏。

但如果一个线程一直在运行,并且其ThreadLocalMap中的 Entry.value 一直指向某个强引用对象,那么这个对象就不会被回收,从而导致内存泄漏。当 Entry 非常多时,可能就会引发更严重的内存溢出问题。

那怎么解决内存泄漏问题呢?

很简单,使用完 ThreadLocal 后,及时调用remove()方法释放内存空间。

try{threadLocal.set(value);// 执行业务操作}finally{threadLocal.remove();// 确保能够执行清理}

remove()方法会将当前线程的 ThreadLocalMap 中的所有 key 为 null 的 Entry 全部清除,这样就能避免内存泄漏问题。

privatevoidremove(ThreadLocal<?>key){Entry[]tab=table;intlen=tab.length;inti=key.threadLocalHashCode&(len-1);for(Entrye=tab[i];e!=null;e=tab[i=nextIndex(i,len)]){if(e.get()==key){e.clear();expungeStaleEntry(i);return;}}}publicvoidclear(){this.referent=null;}

那为什么 key 要设计成弱引用?

弱引用的好处是,当内存不足的时候,JVM 会主动回收掉弱引用的对象。

比如说:

WeakReferencekey=newWeakReference(newThreadLocal());

key 是弱引用,new WeakReference(new ThreadLocal())是弱引用对象,当 JVM 进行垃圾回收时,如果发现了弱引用对象,就会将其回收。

一旦 key 被回收,ThreadLocalMap 在进行 set、get 的时候就会对 key 为 null 的 Entry 进行清理。

总结一下,在 ThreadLocal 被垃圾收集后,下一次访问 ThreadLocalMap 时,Java 会自动清理那些键为 null 的条目(参照源码中的 replaceStaleEntry 方法),这个过程会在执行 ThreadLocalMap 相关操作(如get(),set(),remove())时触发。

你了解哪些 ThreadLocal 的改进方案?

在 JDK 20 Early-Access Build 28 版本中,出现了 ThreadLocal 的改进方案,即ScopedValue

还有 Netty 中的 FastThreadLocal,它是 Netty 对 ThreadLocal 的优化,它内部维护了一个索引常量 index,每次创建 FastThreadLocal 中都会自动+1,用来取代 hash 冲突带来的损耗,用空间换时间。

privatefinalintindex;publicFastThreadLocal(){index=InternalThreadLocalMap.nextVariableIndex();}publicstaticintnextVariableIndex(){intindex=nextIndex.getAndIncrement();if(index<0){nextIndex.decrementAndGet();}returnindex;}

5. ThreadLocalMap 的源码看过吗?

ThreadLocalMap 虽然被叫做 Map,其实它是没有实现 Map 接口的,但是结构还是和 HashMap 比较类似的,主要关注的是两个要素:元素数组散列方法

  • 元素数组

    一个 table 数组,存储 Entry 类型的元素,Entry 是 ThreaLocal 弱引用作为 key,Object 作为 value 的结构。

privateEntry[]table;
  • 散列方法

    散列方法就是怎么把对应的 key 映射到 table 数组的相应下标,ThreadLocalMap 用的是哈希取余法,取出 key 的 threadLocalHashCode,然后和 table 数组长度减一&运算(相当于取余)。

inti=key.threadLocalHashCode&(table.length-1);

这里的 threadLocalHashCode 计算有点东西,每创建一个 ThreadLocal 对象,它就会新增0x61c88647,这个值很特殊,它是斐波那契数也叫黄金分割数hash增量为 这个数字,带来的好处就是hash分布非常均匀

privatestaticfinalintHASH_INCREMENT=0x61c88647;privatestaticintnextHashCode(){returnnextHashCode.getAndAdd(HASH_INCREMENT);}

6. ThreadLocalMap 怎么解决 Hash 冲突的?

我们可能都知道 HashMap 使用了链表来解决冲突,也就是所谓的链地址法。

ThreadLocalMap 没有使用链表,自然也不是用链地址法来解决冲突了,它用的是另外一种方式——开放定址法。开放定址法是什么意思呢?简单来说,就是这个坑被人占了,那就接着去找空着的坑。

如上图所示,如果我们插入一个 value=27 的数据,通过 hash 计算后应该落入第 4 个槽位中,而槽位 4 已经有了 Entry 数据,而且 Entry 数据的 key 和当前不相等。此时就会线性向后查找,一直找到 Entry 为 null 的槽位才会停止查找,把元素放到空的槽中。

在 get 的时候,也会根据 ThreadLocal 对象的 hash 值,定位到 table 中的位置,然后判断该槽位 Entry 对象中的 key 是否和 get 的 key 一致,如果不一致,就判断下一个位置。

7. ThreadLocalMap 扩容机制了解吗?

在 ThreadLocalMap.set()方法的最后,如果执行完启发式清理工作后,未清理到任何数据,且当前散列数组中Entry的数量已经达到了列表的扩容阈值(len*2/3),就开始执行rehash()逻辑:

if(!cleanSomeSlots(i,sz)&&sz>=threshold)rehash();

再着看 rehash()具体实现:这里会先去清理过期的 Entry,然后还要根据条件判断size >= threshold - threshold / 4也就是size >= threshold* 3/4来决定是否需要扩容。

privatevoidrehash(){//清理过期EntryexpungeStaleEntries();//扩容if(size>=threshold-threshold/4)resize();}//清理过期EntryprivatevoidexpungeStaleEntries(){Entry[]tab=table;intlen=tab.length;for(intj=0;j<len;j++){Entrye=tab[j];if(e!=null&&e.get()==null)expungeStaleEntry(j);}}

接着看看具体的resize()方法,扩容后的newTab的大小为老数组的两倍,然后遍历老的 table 数组,散列方法重新计算位置,开放地址解决冲突,然后放到新的newTab,遍历完成之后,oldTab中所有的entry数据都已经放入到newTab中了,然后 table 引用指向newTab

具体代码:

8. 父子线程怎么共享数据?

父线程能用 ThreadLocal 来给子线程传值吗?毫无疑问,不能。那该怎么办?

这时候可以用到另外一个类——InheritableThreadLocal

使用起来很简单,在主线程的 InheritableThreadLocal 实例设置值,在子线程中就可以拿到了。

publicclassInheritableThreadLocalTest{publicstaticvoidmain(String[]args){finalThreadLocalthreadLocal=newInheritableThreadLocal();// 主线程threadLocal.set("不擅技术");//子线程Threadt=newThread(){@Overridepublicvoidrun(){super.run();System.out.println("鄙人三某 ,"+threadLocal.get());}};t.start();}}

那原理是什么呢?

原理很简单,在 Thread 类里还有另外一个变量:

ThreadLocal.ThreadLocalMapinheritableThreadLocals=null;

在 Thread.init 的时候,如果父线程的inheritableThreadLocals不为空,就把它赋给当前线程(子线程)的inheritableThreadLocals

if(inheritThreadLocals&&parent.inheritableThreadLocals!=null)this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/2 22:59:35

消息队列设计:从同步到异步的性能突破

前言 2024年初&#xff0c;我们的订单系统经常出现"超时"问题。用户下单后&#xff0c;系统需要同时调用库存服务、支付服务、通知服务&#xff0c;任何一个服务慢都会导致整个请求超时。 我们决定引入消息队列&#xff0c;将同步调用改为异步处理。这个改造带来了…

作者头像 李华
网站建设 2026/5/2 12:38:05

当AI学会“拍电影“:SkyReels V1如何让你的RTX 4090变身好莱坞工作站

"给我一张照片,我能让它动起来;给我一句话,我能把它拍成电影。"这不是科幻小说的情节,而是SkyReels V1正在做的事情。更酷的是,你不需要A100集群,一张RTX 4090就能让这个魔法在你的桌面上发生。 一、开场白&#xff1a;视频生成的"三座大山"与破局之道 1.…

作者头像 李华
网站建设 2026/5/2 8:30:16

从零入门CANN:揭秘华为昇腾AI计算的核心引擎

# 从零入门CANN&#xff1a;揭秘华为昇腾AI计算的核心引擎> &#x1f4cc; **关键词**&#xff1a;CANN、昇腾AI、Ascend、国产芯片、MindSpore、异构计算 > &#x1f4a1; **适合人群**&#xff1a;AI初学者、高校学生、转行开发者、信创从业者 > ⏱️ **阅读时间…

作者头像 李华
网站建设 2026/4/30 23:05:40

Vue 开发者必看:3 步搞定 dart-sass 替换 node-sass(告别编译慢 +

Vue 开发者必看&#xff1a;3 步搞定 dart-sass 替换 node-sass&#xff08;告别编译慢 Vue 开发者必看&#xff1a;3 步搞定 dart-sass 替换 node-sass&#xff08;告别编译慢 兼容坑&#xff09;引言&#xff1a;为什么我们要和 node-sass 说拜拜技术背景速览&#xff1a;s…

作者头像 李华
网站建设 2026/4/30 23:05:44

.NET进阶——深入理解委托(3)事件入门

为什么我要把事件放在委托这个专题里呢&#xff1f;主要的原因是事件是委托的高级封装。 换句话说&#xff0c;先有委托才有事件&#xff0c;委托是事件的基础&#xff0c;事件是委托的封装。 我们先看一个不用委托的例子&#xff0c;这个代码要求实现这样的功能&#xff1a;小…

作者头像 李华
网站建设 2026/5/3 17:40:55

springboot基于vue的仓库进销存管理系统的可视化分析系统 97lgaf55

目录已开发项目效果实现截图开发技术系统开发工具&#xff1a;核心代码参考示例1.建立用户稀疏矩阵&#xff0c;用于用户相似度计算【相似度矩阵】2.计算目标用户与其他用户的相似度系统测试总结源码文档获取/同行可拿货,招校园代理 &#xff1a;文章底部获取博主联系方式&…

作者头像 李华