早上好,各位新老读者们,我是七淅(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. java-字节流-字符流

    I/O叙述 FileOutputStream类字节输出流的介绍: 写入数据的原理 java程序-->JVM(java虚拟机)--->OS(操作系统)---->OS调用写数据的方法-- ...

  2. synchronized、volatile、CAS 比较?

    1.synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞. 2.volatile 提供多线程共享变量可见性和禁止指令重排序优化. 3.CAS 是基于冲突检测的乐观锁(非阻塞)

  3. Spring源码分析笔记--AOP

    核心类&方法 BeanDefinition Bean的定义信息,封装bean的基本信息,从中可以获取类名.是否是单例.是否被注入到其他bean中.是否懒加载.bean依赖的bean的名称等. ...

  4. numpy入门—Numpy的核心array对象以及创建array的方法

    Numpy的核心array对象以及创建array的方法 array对象的背景: Numpy的核心数据结构,就叫做array就是数组,array对象可以是一维数组,也可以是多维数组: Python的Li ...

  5. AD18布线技巧

    3. 快乐打孔模式(颜色配置)PCB 设计完成后,补回流孔,需要打开多层,软件设置如下: 设置方法: 转载原文链接未知

  6. TextView显示html样式的文字

    项目需求: TextView显示一段文字,格式为:白雪公主(姓名,字数不确定)向您发来了2(消息个数,不确定)条消息 这段文字中名字和数字的长度是不确定的,还要求名字和数字各自有各自的颜色. 一开始我 ...

  7. CentOS7 Network Setting

    #display devices[root@localhost ~]# nmcli d #set ipv4 address[root@localhost ~]# nmcli c modify eth0 ...

  8. 手撕spring核心源码,彻底搞懂spring流程

    引子 十几年前,刚工作不久的程序员还能过着很轻松的日子.记得那时候公司里有些开发和测试的女孩子,经常有问题解决不了的,不管什么领域的问题找到我,我都能帮她们解决.但是那时候我没有主动学习技术的意识,只 ...

  9. 帝国CMS批量提取正文内容到简介

    最近接到一个帝国CMS模板改版项目,自带的数据可能是采集的,以前的简介字段内容只截取了60个字,新模板的简介60字符太少了,不美观,想让简介都截取200个字,怎么批量修改呢,文章太多了手动改肯定不行, ...

  10. springdata jpa多表查询的方式

    方式一:使用@Query注解方式查询主要有JPQL方式,也就是面向对象的方式,这种情况下查表其实查的是对象,字段是实体中的属性,该方式可以直接映射到实体,如下图. 使用jpql的方式模糊查询时候不能使 ...