一问一答:启动线程为什么是运行 start () 方法而不是 run () 方法
问:启动线程为什么要调用 start () 方法,而不能直接调用 run () 方法?
答:核心区别在于是否真的创建新线程。
- 如果直接调用 run (),它只是在主线程里执行了一个普通方法,程序还是串行执行,根本没有开启多线程。
- 只有调用 start () 方法,JVM 才会真正创建一个新的工作线程,由新线程去执行 run () 里的逻辑,这才实现了多线程并发。
问:能不能详细解释一下 start () 方法内部做了什么?
答:start () 方法是 JVM 和操作系统配合工作的:
问:如果多次调用 start () 方法会怎么样?
答:多次调用 start () 会直接抛出IllegalThreadStateException异常。因为线程一旦启动完成,状态就会变为终止,不能再次被启动,只能新建一个线程对象再调用一次。
问:那 run () 方法存在的意义是什么?
答:run () 方法是线程的执行逻辑载体。它本质上是一个普通的方法,当新线程启动后,JVM 会自动调用它来执行业务代码。我们重写 run (),就是告诉新线程启动后要去做什么事。
一问一答:守护线程了解吗
问:Java 里的守护线程和用户线程有什么区别?
答:可以把 JVM 比作一家公司:
- 用户线程就是正式员工,比如 main 线程,公司要等所有正式员工都下班了才会关门。
- 守护线程就是保洁阿姨,比如垃圾回收线程,只要正式员工都走了,不管保洁有没有干完活,公司都会直接锁门下班。
问:那守护线程的核心作用是什么?
答:守护线程是为用户线程提供后台服务的,比如 JVM 里的垃圾回收线程,它会在后台默默清理内存,不需要我们手动管理。只要还有一个用户线程在运行,JVM 就会一直工作;当最后一个用户线程结束时,JVM 会直接退出,守护线程也会被强制终止。
问:怎么把一个线程设置成守护线程?
答:在调用 start () 方法之前,调用setDaemon(true)就可以把线程标记为守护线程。需要注意的是,守护线程里创建的子线程,默认也是守护线程。
问:使用守护线程有什么要注意的地方?
答:因为 JVM 退出时不会等守护线程执行完,所以不要在守护线程里做需要保证完整性的操作,比如写文件、数据库事务,否则可能因为突然终止导致数据丢失。
一问一答:线程间有哪些通信方式
问:Java 里线程间有哪些常见的通信方式?
答:主要有四种方式,就像不同场景下的 “传话方式”:
问:volatile 和 synchronized 关键字怎么实现通信?
答:
- volatile 就像贴在共享变量上的 “实时公告”,所有线程都必须从共享内存读,修改后也必须立刻写回,保证大家看到的值是一致的,间接实现数据传递。
- synchronized 就像给共享资源上了 “单间锁”,同一时间只有一个线程能进,保证了数据的可见性和排他性,避免多线程同时修改导致混乱。
问:等待 / 通知机制是什么?
答:这是 Java 内置的 “喊人干活” 机制,用 wait () 和 notify () 实现:
- 一个线程修改完数据后,调用 notify () 喊醒等待的线程;
- 另一个线程发现条件不满足时,调用 wait () 主动睡觉,直到被唤醒。就像厨师做好菜后喊服务员上菜,服务员没菜时就休息等通知。
问:管道输入 / 输出流是什么?
答:这是线程间的 “内存快递通道”,专门用来在线程之间传数据,和文件 / 网络流不同,它的传输媒介是内存。有两种类型:面向字节的 PipedOutputStream/PipedInputStream,面向字符的 PipedReader/PipedWriter,就像线程之间的 “内存管道”,一个往里写,一个往外读。
问:Thread.join () 怎么实现通信?
答:join () 就是 “等你干完我再干”:
- 线程 A 调用 thread.join (),就会一直等 thread 线程执行完,再继续自己的逻辑。
- 因为 thread 线程已经执行完了,它修改的数据肯定能被线程 A 读到,这就实现了线程间的结果传递。
一问一答:ThreadLocal 是什么
问:ThreadLocal 到底是什么?
答:ThreadLocal 就是线程本地变量,本质是给每个线程都发一份变量副本,就像公司给每个员工发一个专属笔记本:
- 员工(线程)只能读写自己的笔记本(本地副本),看不到别人的内容;
- 多个员工操作同一个变量时,实际操作的是自己的副本,完全互不干扰,天然避免了线程安全问题。
问:ThreadLocal 的基本用法是什么?
答:用法很简单,分三步:
public static ThreadLocal<String> localVariable = new ThreadLocal<>();
localVariable.set("test");
localVariable.get();
问:ThreadLocal 的核心作用是什么?
答:核心是线程隔离,解决两个场景问题:
- 避免多线程并发修改共享变量导致的线程安全问题;
- 在同一个线程内传递上下文数据,比如 Web 开发里传递用户信息、请求 ID,不用在方法间层层传递参数。
问:使用 ThreadLocal 要注意什么?
答:一定要记得手动清理,调用remove()方法,否则会出现内存泄漏:
- ThreadLocal 的副本是绑定在线程上的,如果线程是复用的(比如线程池),旧数据会一直存在;
- 不清理的话,不仅会占内存,还可能导致下一个复用线程读到脏数据。
一问一答:ThreadLocal 的应用场景
问:ThreadLocal 在实际开发中有哪些典型应用场景?
答:主要有 3 个核心场景,本质都是 “线程隔离,让每个线程管好自己的事”:
问:线程池技术里怎么用 ThreadLocal?
答:线程池就像一个共享的办公室,大家共用资源。为了避免不同员工(线程)互相干扰,用 ThreadLocal 给每个员工发一个专属办公桌:
- 线程池里的多个任务并发执行时,ThreadLocal 存储每个线程独有的数据;
- 每个线程只能操作自己的副本,不会和别的线程搞混,安全共享线程池资源。
问:Web 应用里为什么要用 ThreadLocal?
答:Web 应用里每个请求就是一个线程,请求嵌套调用很深。ThreadLocal 就像给当前请求发一个专属 “随身公文包”:
- 存储用户 ID、请求时间、TraceId 等上下文信息;
- 同一个请求的所有方法都能从公文包里拿数据,不同请求之间完全独立,避免层层传参。
问:数据库连接怎么用到 ThreadLocal?
答:为了避免每个线程都重复创建、销毁数据库连接,用连接池复用连接。ThreadLocal 把连接和当前线程绑定,就像给每个线程发一把专属数据库钥匙:
- 每个线程拿到自己唯一的连接,操作数据库不混乱;
- 避免多线程同时共用一个连接导致数据错乱和线程安全问题。
问:这三个场景的核心共同点是什么?
答:都是为了解决多线程并发冲突和线程内上下文传递的问题。
- 在共享资源环境下,通过线程隔离保证数据安全;
- 在复杂调用链路中,实现线程内数据的全局便捷传递。
一问一答:ThreadLocal 是怎么实现的
问:ThreadLocal 的底层实现原理是什么?
答:可以把 ThreadLocal 理解成一个 “钥匙”,真正存数据的是每个线程自己的 “保险柜”(ThreadLocalMap),核心逻辑如下:
问:set () 方法里到底做了什么?
答:当我们调用set()存值时,流程是:
问:ThreadLocalMap 是什么?它存在哪里?
答:ThreadLocalMap是 Thread 类里的一个成员变量,叫threadLocals。
- 每个线程都有自己独立的ThreadLocalMap,就像每个人都有自己的保险柜;
- 这个 map 里存的是多个Entry节点,每个节点对应一个 ThreadLocal 变量和它的值。
问:Entry 节点是什么样的?
答:Entry继承了WeakReference<ThreadLocal<?>>,是一个弱引用:
- key 是 ThreadLocal 的弱引用;
- value 是我们存进去的具体值;
- 用弱引用是为了在 ThreadLocal 不再被使用时,能被 GC 回收,避免内存泄漏。
问:为什么这样设计就能实现线程隔离?
答:因为每个线程操作的都是自己的ThreadLocalMap:
- 存值时,往自己的 map 里存;
- 取值时,从自己的 map 里找;
- ThreadLocal 本身只是一个 “钥匙”,不存数据,只是用来定位 map 里的 entry,所以天然实现了线程隔离。
问:这种设计有什么好处?
答:
一问一答:ThreadLocal 内存泄漏是怎么回事
问:ThreadLocal 为什么会出现内存泄漏?
答:核心原因是key 被回收了,但 value 还留着:
- ThreadLocalMap 里的 Entry,key 是 ThreadLocal 的弱引用,GC 时只要 ThreadLocal 没其他强引用,key 就会被回收;
- 但 Entry 的 value 是强引用,只要线程还活着(比如线程池里的线程复用),ThreadLocalMap 就一直存在,value 就没法被回收;
- 时间久了,这些 “key 没了、value 还在” 的 Entry 越积越多,就造成了内存泄漏。
问:怎么解决 ThreadLocal 的内存泄漏?
答:最直接的办法是用完就调用 remove () 方法清理:
java
运行
ThreadLocal<String> localVariable = new ThreadLocal();
try {
localVariable.set("数据");
// 业务逻辑
} finally {
// 必须在finally里调用,保证一定执行
localVariable.remove();
}
- remove () 会直接把当前 ThreadLocal 对应的 Entry 从 map 里删掉,连 key 带 value 一起清理,彻底避免泄漏。
问:那为什么 Entry 的 key 要设计成弱引用?
答:弱引用其实是为了减少内存泄漏的风险:
- 如果 key 是强引用,就算外部 ThreadLocal 引用被销毁了,key 还强引用着 ThreadLocal 实例,导致 ThreadLocal 永远没法被 GC,泄漏更严重;
- 弱引用能让 ThreadLocal 在没人用的时候被回收,至少 key 会消失,后续在 map 扩容 / 清理时,还能把这些 key 为 null 的 Entry 的 value 也清理掉,算是一种兜底保护。
问:线程池场景下为什么更容易内存泄漏?
答:线程池里的线程是复用的,生命周期很长,不会随着任务结束而销毁:
- 任务结束后如果没调用 remove (),线程的 ThreadLocalMap 里的 value 就一直占着内存;
- 下一个任务复用这个线程时,还可能读到上一个任务的脏数据,既占内存又有业务风险。
一问一答:ThreadLocalMap 的结构了解吗
问:ThreadLocalMap 的结构是什么样的?
答:ThreadLocalMap 虽然叫 Map,但没实现 Map 接口,结构和 HashMap 类似,核心是两个部分:
- 元素数组:一个Entry[] table数组,存 Entry 类型的元素,Entry 里 key 是 ThreadLocal 的弱引用,value 是存的数据。
- 散列方法:用哈希取余法,把 ThreadLocal 对象映射到数组下标。
问:散列方法具体是怎么算的?
答:计算下标的公式是:
java
运行
int i = key.threadLocalHashCode & (table.length – 1);
- threadLocalHashCode是每个 ThreadLocal 对象的哈希值;
- 和数组长度减一做&运算,等价于取余,效率更高;
- 这个哈希值是用斐波那契数(黄金分割数0x61c88647)作为增量生成的,能让哈希分布非常均匀,减少冲突。
问:为什么要用斐波那契数做哈希增量?
答:斐波那契数(黄金分割数)的特点是能让生成的哈希值分布特别均匀,这样不同 ThreadLocal 对象映射到数组不同位置的概率更高,减少哈希冲突,让 ThreadLocalMap 的存取效率更稳定。
问:Entry 是什么结构?和 HashMap 的 Entry 有什么不一样?
答:
- Entry 继承WeakReference<ThreadLocal<?>>,key 是 ThreadLocal 的弱引用,value 是强引用;
- HashMap 的 Entry 是强引用 key 和 value;
- 弱引用 key 的好处是,当 ThreadLocal 没有其他强引用时,GC 能自动回收 key,减少内存泄漏风险。
问:ThreadLocalMap 和 HashMap 的核心区别是什么?
答:
一问一答:ThreadLocalMap 怎么解决 Hash 冲突(生动形象版)
问:ThreadLocalMap 是怎么解决 Hash 冲突的?
答:ThreadLocalMap 不用 HashMap 那种链表法,而是用开放定址法(线性探测法),可以理解成 “这个坑被占了,就往后找下一个空坑”。
问:具体是怎么操作的?
答:
插入时:先算好下标,如果这个位置已经有 Entry,而且 key 不匹配,就往后逐个找空位置,直到找到 Entry 为 null 的槽位,把新元素放进去。
查询时:先定位到计算出的下标,如果这个位置的 Entry key 和要查询的 key 不一致,就继续往后逐个检查下一个位置,直到找到匹配的 key 或者遇到空槽。
问:和 HashMap 的链表法比,这种方式有什么优缺点?
答:
优点:结构更简单,没有链表 / 红黑树的额外开销,适合 ThreadLocalMap 这种数据量小、冲突少的场景;哈希分布均匀时,探测效率很高。
缺点:冲突严重时,探测路径会变长,查询效率下降;删除元素时不能直接删,需要做 “惰性删除”,否则会导致后面的元素找不到。
问:为什么 ThreadLocalMap 要选开放定址法,而不是链表法?
答:
ThreadLocal 的哈希值用黄金分割数生成,分布非常均匀,Hash 冲突概率极低,线性探测的性能损耗很小;
每个线程的 ThreadLocalMap 里元素数量通常不多,线性探测足够高效;
结构简单,内存占用少,符合 JVM 底层轻量设计的目标。
一问一答:ThreadLocalMap 扩容机制了解吗
问:ThreadLocalMap 什么时候会触发扩容?
答:扩容触发分两步判断:
问:rehash () 里具体做了什么?
答:rehash()会先清理过期 Entry,再判断是否真的需要扩容:
问:resize () 是怎么扩容的?
答:扩容流程很清晰:
-
- 如果 Entry 的 key 为 null,就把 value 置为 null 帮助 GC;
- 如果 key 有效,就用散列方法重新计算在新数组的下标,用开放定址法解决冲突,放到新数组里;
问:为什么要先清理过期 Entry 再扩容?
答:这是为了尽量避免不必要的扩容:
- 很多时候,清理掉 key 为 null 的过期 Entry 后,有效 Entry 数量会下降,可能就达不到扩容阈值了;
- 既减少了内存占用,又避免了频繁扩容带来的性能开销。
问:和 HashMap 的扩容比,ThreadLocalMap 扩容有什么不同?
答:
- 触发条件:HashMap 是size >= 阈值就扩容,ThreadLocalMap 要先清理再判断size >= 阈值*3/4才扩容;
- 扩容倍数:都是扩为原来 2 倍;
- 重哈希:HashMap 会重新哈希,ThreadLocalMap 也是重新散列,但用的是开放定址法解决冲突;
- 清理逻辑:ThreadLocalMap 扩容前会主动清理过期 Entry,HashMap 没有这个逻辑。
一问一答:父子线程怎么共享数据
问:普通 ThreadLocal 能让父子线程共享数据吗?
答:不能,普通 ThreadLocal 是线程隔离的,子线程拿不到父线程里的值。要实现父子线程数据共享,需要用InheritableThreadLocal。
问:InheritableThreadLocal 怎么用?
答:用法和普通 ThreadLocal 几乎一样:
示例代码:
java
运行
public class InheritableThreadLocalTest {
public static void main(String[] args) {
final InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
// 父线程存值
threadLocal.set("main");
// 子线程启动后可以直接获取
Thread t = new Thread(() -> {
System.out.println("父线程数据: " + threadLocal.get());
});
t.start();
}
}
问:它的实现原理是什么?
答:核心在Thread类里多了一个成员变量:
java
运行
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
在Thread.init()初始化子线程时:
问:和普通 ThreadLocal 比,InheritableThreadLocal 有什么区别?
答:
- ThreadLocal:完全线程隔离,父子线程互不感知;
- InheritableThreadLocal:子线程会继承父线程的inheritableThreadLocals,实现父子线程数据共享;
- 注意:继承只发生在子线程创建时,如果父线程之后修改了值,子线程不会同步感知到。



