早上好,各位新老读者们,我是七淅(xī)。

今天和大家分享的是面试常驻嘉宾:ThreadLocal

当初鹅厂一面就有问到它,问题的答案在下面正文的第 2 点。

1. 底层结构

ThreadLocal 底层有一个默认容量为 16 的数组组成,k 是 ThreadLocal 对象的引用,v 是要放到 TheadLocal 的值

public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
} void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
} ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}

数组类似为 HashMap,对哈希冲突的处理不是用链表/红黑树处理,而是使用链地址法,即尝试顺序放到哈希冲突下标的下一个下标位置。

该数组也可以进行扩容。

2. 工作原理

一个 ThreadLocal 对象维护一个 ThreadLocalMap 内部类对象,ThreadLocalMap 对象才是存储键值的地方。

更准确的说,是 ThreadLocalMap 的 Entry 内部类是存储键值的地方

见源码 set(),createMap() 可知。

因为一个 Thread 对象维护了一个 ThreadLocal.ThreadLocalMap 成员变量,且 ThreadLocal 设置值时,获取的 ThreadLocalMap 正是当前线程对象的 ThreadLocalMap

// 获取 ThreadLocalMap 源码
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

所以每个线程对 ThreadLocal 的操作互不干扰,即 ThreadLocal 能实现线程隔离

3. 使用

ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("七淅在学Java");
Integer i = threadLocal.get()
// i = 七淅在学Java

4. 为什么 ThreadLocal.ThreadLocalMap 底层是长度 16 的数组呢?

对 ThreadLocal 的操作见第 3 点,可以看到 ThreadLocal 每次 set 方法都是对同个 key(因为是同个 ThreadLocal 对象,所以 key 肯定都是一样的)进行操作。

如此操作,看似对 ThreadLocal 的操作永远只会存 1 个值,那用长度为 1 的数组它不香吗?为什么还要用 16 长度呢?

好了,其实这里有个需要注意的地方,ThreadLocal 是可以存多个值的

那怎么存多个值呢?看如下代码:

// 在主线程执行以下代码:
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("七淅在学Java");
ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
threadLocal2.set("七淅在学Java2");

按代码执行后,看着是 new 了 2 个 ThreadLocal 对象,但实际上,数据的存储都是在同一个 ThreadLocal.ThreadLocalMap 上操作的

再次强调:ThreadLocal.ThreadLocalMap 才是数据存取的地方,ThreadLocal 只是 api 调用入口)。真相在 ThreadLocal 类源码的 getMap()

因此上述代码最终结果就是一个 ThreadLocalMap 存了 2 个不同 ThreadLocal 对象作为 key,对应 value 为 七淅在学Java、七淅在学Java2。

我们再看下 ThreadLocal 的 set 方法

public void set(T value) {
Thread t = Thread.currentThread();
// 这里每次 set 之前,都会调用 getMap(t) 方法,t 是当前调用 set 方法的线程
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
} // 重点:返回调用 set 方法的线程(例子是主线程)的 ThreadLocal 对象。
// 所以不管 api 调用方 new 多少个 ThreadLocal 对象,它永远都是返回调用线程(例子是主线程)的 ThreadLocal.ThreadLocalMap 对象供调用线程去存取数据。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
} // t.threadLocals 的声明如下
ThreadLocal.ThreadLocalMap threadLocals = null; // 仅有一个构造方法
public ThreadLocal() {
}

5. 数据存放在数组中,那如何解决 hash 冲突问题

使用链地址法解决。

具体怎么解决呢?看看执行 get、set 方法的时候:

  • set:

    • 根据 ThreadLocal 对象的 hash 值,定位到 ThreadLocalMap 数组中的位置。
    • 如果位置无元素则直接放到该位置
    • 如果有元素
      • 且数组的 key 等于该 ThreadLocal,则覆盖该位置元素
      • 否则就找下一个空位置,直到找到空或者 key 相等为止。
  • get:
    • 根据 ThreadLocal 对象的 hash 值,定位到 ThreadLocalMap 数组中的位置。
    • 如果不一致,就判断下一个位置
    • 否则则直接取出
// 数组元素结构
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}

6. ThreadLocal 的内存泄露隐患

三个前置知识:

  • ThreadLocal 对象维护一个 ThreadLocalMap 内部类
  • ThreadLocalMap 对象又维护一个 Entry 内部类,并且该类继承弱引用 WeakReference<ThreadLocal<?>>,用来存放作为 key 的 ThreadLocal 对象(可见最下方的 Entry 构造方法源码),可见最后的源码部分。
  • 不管当前内存空间足够与否,GC 时 JVM 会回收弱引用的内存

因为 ThreadLocal 作为弱引用被 Entry 中的 Key 变量引用,所以如果 ThreadLocal 没有外部强引用来引用它,那么 ThreadLocal 会在下次 JVM 垃圾收集时被回收。

这个时候 Entry 中的 key 已经被回收,但 value 因为是强引用,所以不会被垃圾收集器回收。这样 ThreadLocal 的线程如果一直持续运行,value 就一直得不到回收,导致发生内存泄露。

如果想要避免内存泄漏,可以使用 ThreadLocal 对象的 remove() 方法

7. 为什么 ThreadLocalMap 的 key 是弱引用

static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value; Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}

为什么要这样设计,这样分为两种情况来讨论:

  • key 使用强引用:只有创建 ThreadLocal 的线程还在运行,那么 ThreadLocalMap 的键值就都会内存泄漏,因为 ThreadLocalMap 的生命周期同创建它的 Thread 对象。
  • key 使用弱引用:是一种挽救措施,起码弱引用的值可以被及时 GC,减轻内存泄漏。另外,即使没有手动删除,作为键的 ThreadLocal 也会被回收。因为 ThreadLocalMap 调用 set、get、remove 时,都会先判断之前该 value 对应的 key 是否和当前调用的 key 相等。如果不相等,说明之前的 key 已经被回收了,此时 value 也会被回收。因此 key 使用弱引用是最优的解决方案。

8. (父子线程)如何共享 ThreadLocal 数据

  1. 主线程创建 InheritableThreadLocal 对象时,会为 t.inheritableThreadLocals 变量创建 ThreadLocalMap,使其初始化。其中 t 是当前线程,即主线程
  2. 创建子线程时,在 Thread 的构造方法,会检查其父线程的 inheritableThreadLocals 是否为 null。从第 1 步可知不为 null,接着 将父线程的 inheritableThreadLocals 变量值复制给这个子线程。
  3. InheritableThreadLocal 重写了 getMap, createMap, 使用的都是 Thread.inheritableThreadLocals 变量

如下:

public class InheritableThreadLocal<T> extends ThreadLocal<T> 

关键源码:

第 1 步:对 InheritableThreadLocal 初始化
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
} 第 2 步:创建子线程时,判断父线程的 inheritableThreadLocals 是否为空。非空进行复制
// Thread 构造方法中,一定会执行下面逻辑
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); 第 3 步:使用对象为第 1 步创建的 inheritableThreadLocals 对象
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
} 示例:
// 结果:能够输出「父线程-七淅在学Java」
ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("父线程-七淅在学Java");
Thread t = new Thread(() -> System.out.println(threadLocal.get()));
t.start(); // 结果:null,不能够输出「子线程-七淅在学Java」
ThreadLocal threadLocal2 = new InheritableThreadLocal();
Thread t2 = new Thread(() -> {
threadLocal2.set("子线程-七淅在学Java");
});
t2.start();
System.out.println(threadLocal2.get());

文章首发公众号:七淅在学Java ,持续原创输出 Java 后端干货。

如果对你有帮助的话,可以给个赞再走吗

有关 ThreadLocal 的一切的更多相关文章

  1. ThreadLocal简单理解

    在java开源项目的代码中看到一个类里ThreadLocal的属性: private static ThreadLocal<Boolean> clientMode = new Thread ...

  2. Android线程管理之ThreadLocal理解及应用场景

    前言: 最近在学习总结Android的动画效果,当学到Android属性动画的时候大致看了下源代码,里面的AnimationHandler存取使用了ThreadLocal,激起了我很大的好奇心以及兴趣 ...

  3. Threadlocal使用Case

    Threadlocal能够为每个线程分配一份单独的副本,使的线程与线程之间能够独立的访问各自副本.Threadlocal 内部维护一个Map,key为线程的名字,value为对应操作的副本. /** ...

  4. 多线程映射工具——ThreadLocal

    ThreadLocal相当于一个Map<Thread, T>,各线程使用自己的线程对象Thread.currentThread()作为键存取数据,但ThreadLocal实际上是一个包装了 ...

  5. ThreadLocal 工作原理、部分源码分析

    1.大概去哪里看 ThreadLocal 其根本实现方法,是在Thread里面,有一个ThreadLocal.ThreadLocalMap属性 ThreadLocal.ThreadLocalMap t ...

  6. ThreadLocal<T>的是否有设计问题

    一.吐槽 ThreadLocal<T>明显是.NET从JAVA中来的一个概念,但是这种设计是否出现了问题. 很明显,在JAVA中threadLocal直接是Thread的成员,当然随着th ...

  7. 理解ThreadLocal —— 一个map的key

    作用: 当工作于多线程中的对象使用ThreadLocal维护变量时,threadLocal为每个使用该变量的线程分配一个独立的变量副本. 接口方法: protected T initialValue( ...

  8. JavaSe:ThreadLocal

    JDK中有一个ThreadLocal类,使用很方便,但是却很容易出现问题.究其原因, 就是对ThreadLocal理解不到位.最近项目中,出现了内存泄漏的问题.其中就有同事在使用ThreadLocal ...

  9. 0041 Java学习笔记-多线程-线程池、ForkJoinPool、ThreadLocal

    什么是线程池 创建线程,因为涉及到跟操作系统交互,比较耗费资源.如果要创建大量的线程,而每个线程的生存期又很短,这时候就应该使用线程池了,就像数据库的连接池一样,预先开启一定数量的线程,有任务了就将任 ...

  10. ThreadLocal 源码剖析

    ThreadLocal是Java语言提供的用于支持线程局部变量的类.所谓的线程局部变量,就是仅仅只能被本线程访问,不能在线程之间进行共享访问的变量(每个线程一个拷贝).在各个Java web的各种框架 ...

随机推荐

  1. 学习saltstack (三)

    salt是一个新的基础平台管理工具,2011-02-20诞生,创造者Thoms SHatch,起名salt原因生活中常见.易记,使用saltstack.com原因这个域名没有被注册,Because s ...

  2. elasticsearch 5.6.7在线安装ik分词,亲测有效

    官网的在线安装命令 ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/rele ...

  3. Python模块导入方式

    import导入方式 from...import导入方式  from...import... 导入模块相当于在此文件中写了所导入函数名(对比c/c++中的.h文件来理解),所以在之后使用导入的函数直接 ...

  4. Web缓存总结

    web缓存作用 减少网络带宽消耗降低服务器压力减少网络延迟,加快页面打开速度 Web缓存的类型 数据库数据缓存:为了提供查询的性能,会将查询后的数据放到内存中进行缓存,下次查询时,直接从内存缓存直接返 ...

  5. 界面跳转+信息传递+AS中如何将ADV转移到其他盘中

    今日所学:界面跳转 信息传递 遇到的问题: 昨天遇到不能新建java类,在网上百度了很多,大多原因是没有新建java类的模板,但是我有,换了一个新的新建的方式后,发现虽然能建立了,但在测试时还是不能页 ...

  6. springcloud报错:org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'armeriaServer' defined in class path resource

    spring boot配置zipkin 无法启动 加入 Zipkin Server 由于需要收集 Spring Cloud 系统的跟踪信息,以便及时地发现系统中出现的延迟升高问题并找出系统性能瓶颈的根 ...

  7. Java报错:Failed to execute goal org.eclipse.jetty:jetty-maven-plugin:9.4.26.v20200117:run (default-cli) on project ssm-mybatis-plus: Failure

    修改一下端口就好了,不要用80端口. <plugin> <groupId>org.eclipse.jetty</groupId> <!--嵌入式Jetty的M ...

  8. 基于Spring接口,集成Caffeine+Redis两级缓存

    原创:微信公众号 码农参上,欢迎分享,转载请保留出处. 在上一篇文章Redis+Caffeine两级缓存,让访问速度纵享丝滑中,我们介绍了3种整合Caffeine和Redis作为两级缓存使用的方法,虽 ...

  9. LC-19

    19. 删除链表的倒数第 N 个结点 思路基本直接出来,双指针,IndexFast 和 IndexSlow 中间相隔 N - 1, 这样 IndexFast 到了最后,IndexSlow 自然就是倒数 ...

  10. maven打包jar到本地仓库

    1.执行如下命令 mvn install:install-file -Dfile=guava-28.2-jre.jar -DgroupId=com.google.guava -DartifactId= ...