早上好,各位新老读者们,我是七淅(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. kafka 的高可用机制是什么?

    这个问题比较系统,回答出 kafka 的系统特点,leader 和 follower 的关系,消息 读写的顺序即可.

  2. 数据仓库(5)数仓Kimball与Inmon架构的对比

    数据仓库主要有四种架构,Kimball的DW/BI架构.独立数据集市架构.辐射状企业信息工厂Inmon架构.混合Inmon与Kimball架构.不过不管是那种架构,基本上都会使用到维度建模. < ...

  3. Nuxt.js的踩坑指南(常见问题汇总)

    本文会不定期更新在nuxt.js中遇到的问题进行汇总.转发请注明出处,尊重作者,谢谢! 强烈推荐作者文档版踩坑指南,点击跳转踩坑指南 在Nuxt的官方文档中,中文文档和英文文档都存在着不小的差异. 1 ...

  4. android JS 互相通讯

    1.android中利用webview调用网页上的js代码. Android 中可以通过webview来实现和js的交互,在程序中调用js代码,只需要将webview控件的支持js的属性设置为true ...

  5. 【uniapp 开发】UniPush

    App.vue export default { onLaunch: function() { // #ifdef APP-PLUS const _self = this; const _handle ...

  6. IO流入门+简单案例实现

    IO流 总结内容 1. IO流是什么 2. 字符流和字节流 3. File常用API(前面类型为返回类型) 4. 编码转换 5. IO流实现流程 6. 输入输出流简单实现 7. 输入输出流简单实现 总 ...

  7. vue中对element-ui框架中el-table的列的每一项数据进行操作

    vue中使用element table,表格参数格式化formatter 后台返回对应的数字, 那肯定不能直接显示数字,这时候就要对 表格进行数据操作 如图: 代码: methods: { //状态改 ...

  8. 前端如何通过js判断浏览器的类型(忽略版本)web html css javascript

    每个页面浏览器会实例出一个window对象,在window对象下有一个属性navigator,navigator本身是一个对象,navigator对象上有一个属性userAgent里面包含了当前浏览器 ...

  9. 关于vue中v-for的键值顺序

    在学习vue2.0时,关于处理v-for键值顺序时发现的问题: <body> <!-- 普通循环 --> <!-- {{num}} --> <!-- 列表循环 ...

  10. Python入门-系统模块time

    1.time模块 时间戳:1970年,1月1日开始时间元祖:包含日期,时间,保存日期结构的元祖对象格式化时间日期:按照指定的标记进行格式化处理 时间戳 import time time_num = t ...