鹅厂一面,有关 ThreadLocal 的一切

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);elsecreateMap(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);elsecreateMap(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:

    • 且数组的 key 等于该 ThreadLocal,则覆盖该位置元素

    • 否则就找下一个空位置,直到找到空或者 key 相等为止。

    • 根据 ThreadLocal 对象的 hash 值,定位到 ThreadLocalMap 数组中的位置。

    • 如果位置无元素则直接放到该位置

    • 如果有元素

  • 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<?>> {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());

66410d587dcd9d37f8bc4ea96b09ee31.gif

往期推荐

3019852ad4bff9b9e414b4cf634ee3fc.png

面试突击55:delete、drop、truncate有什么区别?


bb82d7bbb6e5dd0c6e13492193958ff9.png

面渣逆袭:MyBatis连环20问,这谁顶得住?


89c5ef7e1af24c951d18c730f36ffe25.png

面渣逆袭:RocketMQ二十三问


9d97a54c95234a4d77e016bd787716b3.gif

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/544108.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

面试突击58:truncate、delete和drop的6大区别!

作者 | 磊哥来源 | Java面试真题解析&#xff08;ID&#xff1a;aimianshi666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;在 MySQL 中&#xff0c;使用 truncate、delete 和 drop 都可以实现表删除&#xff0c;但它们 3 个的使用场景和执行…

智力游戏

【Description】whitecloth 最近迷上了一个你小时候已经玩厌了的游戏&#xff1a;移火柴棒。他现在吵着要你陪他玩&#xff0c;你没有办法&#xff0c;只好写一个程序来完成这个工作了。你被给出了一个火柴拼成的等式&#xff0c;比如说下面这个&#xff1a;&#xff08; 5 7 …

面渣逆袭:MySQL六十六问!建议收藏

基础MySQ Logo作为SQL Boy&#xff0c;基础部分不会有人不会吧&#xff1f;面试也不怎么问&#xff0c;基础掌握不错的小伙伴可以跳过这一部分。当然&#xff0c;可能会现场写一些SQL语句&#xff0c;SQ语句可以通过牛客、LeetCode、LintCode之类的网站来练习。1. 什么是内连接…

try-with-resources 中的一个坑,注意避让

小伙伴们好呀&#xff0c;昨天复盘以前做的项目&#xff08;大概有一年了&#xff09;&#xff0c;看到这个 try-catch &#xff0c;又想起自己之前掉坑的这个经历 &#xff0c;弄了个小 demo 给大家感受下~ &#x1f604;问题1一个简单的下载文件的例子。这里会出现什么情况…

第 二 十 八 天 :LB 负 载 均 衡 搭 建 之 LVS

小Q&#xff1a;抱怨&#xff0c;是一种负能量&#xff0c;犹如搬起石头砸自己的脚&#xff0c;与人无益&#xff0c;于己不利&#xff0c;于事无补 前面我们介绍了HA高可用集群&#xff0c;今天我们来了解下LB负载均衡集群&#xff0c;在学习完基本的搭建后&#xff0c;在扩展…

一个依赖搞定Spring Boot 配置文件脱敏

经常会遇到这样一种情况&#xff1a;项目的配置文件中总有一些敏感信息&#xff0c;比如数据源的url、用户名、密码....这些信息一旦被暴露那么整个数据库都将会被泄漏&#xff0c;那么如何将这些配置隐藏呢&#xff1f;今天介绍一种方案&#xff0c;让你在无感知的情况下实现配…

js ‘use strict’详解

2019独角兽企业重金招聘Python工程师标准>>> 一、概述 除了正常运行模式&#xff0c;ECMAscript 5添加了第二种运行模式&#xff1a;"严格模式"&#xff08;strict mode&#xff09;。顾名思义&#xff0c;这种模式使得Javascript在更严格的条件下运行。 …

如何优雅的写 Controller 层代码?

本篇主要要介绍的就是controller层的处理&#xff0c;一个完整的后端请求由4部分组成&#xff1a;1. 接口地址(也就是URL地址)、2. 请求方式(一般就是get、set&#xff0c;当然还有put、delete)、3. 请求数据(request&#xff0c;有head跟body)、4. 响应数据(response)本篇将解…

面试突击60:什么情况会导致 MySQL 索引失效?

作者 | 磊哥来源 | Java面试真题解析&#xff08;ID&#xff1a;aimianshi666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;为了验证 MySQL 中哪些情况下会导致索引失效&#xff0c;我们可以借助 explain 执行计划来分析索引失效的具体场景。…

使用PHP建立SVN的远程钩子,使用exec命令自动更新SVN的代码

2019独角兽企业重金招聘Python工程师标准>>> 本操作需要使用到php执行sudo命令的权限&#xff0c;相关设置可以参考&#xff1a;apache/Nginx下的PHP/Ruby执行sudo权限的系统命令 通过Svn的钩子功能&#xff0c;可以在我们执行SVN操作时&#xff0c;同时自动执行一些…

Java 中 for 和 foreach 哪个性能高?

作为程序员每天除了写很多 if else 之外&#xff0c;写的最多的也包含 for 循环了&#xff0c;都知道我们 Java 中常用的 for 循环有两种方式&#xff0c;一种是使用 for loop&#xff0c;另一种是使用 foreach&#xff0c;那如果问你&#xff0c;这两种方式哪一种效率最高&…

阿里出品,SpringBoot自动化部署神器!

最近发现一款阿里出品的IDEA插件CloudToolkit&#xff0c;不仅支持直接打包应用部署到远程服务器上&#xff0c;而且还能当终端工具使用。试用了一把这个插件&#xff0c;非常不错&#xff0c;推荐给大家&#xff01;装上这个插件&#xff0c;IDEA一站式开发又近了一步&#xf…

聊聊异步编程的 7 种实现方式

最近有很多小伙伴给我留言&#xff0c;能不能总结下异步编程&#xff0c;今天就和大家简单聊聊这个话题。早期的系统是同步的&#xff0c;容易理解&#xff0c;我们来看个例子同步编程当用户创建一笔电商交易订单时&#xff0c;要经历的业务逻辑流程还是很长的&#xff0c;每一…

二进制补码乘法除法_二进制乘法和除法

二进制补码乘法除法1)二进制乘法 (1) Binary Multiplication) Binary numbers can be multiplied using two methods, 二进制数可以使用两种方法相乘&#xff0c; Paper method: Paper method is similar to multiplication of decimal numbers on paper. 纸张方法&#xff1a…

控制JSP头部引入外部文件编译后在第一行

2019独角兽企业重金招聘Python工程师标准>>> 一.错误引入方法 假设当前需要在JSP页面输出xml格式数据&#xff0c;需要引入以下外部文件&#xff0c;通过以下的方式来引入则无法正常输出数据&#xff1a; 访问页面会报错误&#xff1a;xml的声明不在文档的第一行 看…

4种常见的缓存模式,你都知道吗?

概述 在系统架构中&#xff0c;缓存可谓提供系统性能的最简单方法之一&#xff0c;稍微有点开发经验的同学必然会与缓存打过交道&#xff0c;最起码也实践过。如果使用得当&#xff0c;缓存可以减少响应时间、减少数据库负载以及节省成本。但如果缓存使用不当&#xff0c;则可能…

面试突击63:distinct 和 group by有什么区别?

作者 | 磊哥来源 | Java面试真题解析&#xff08;ID&#xff1a;aimianshi666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;在 MySQL 中&#xff0c;最常见的去重方法有两个&#xff1a;使用 distinct 或使用 group by&#xff0c;那它们有什…

从20s优化到500ms,我用了这三招

前言接口性能问题&#xff0c;对于从事后端开发的同学来说&#xff0c;是一个绕不开的话题。想要优化一个接口的性能&#xff0c;需要从多个方面着手。本文将接着接口性能优化这个话题&#xff0c;从实战的角度出发&#xff0c;聊聊我是如何优化一个慢查询接口的。上周我优化了…

面试拆解:系统上线后CPU急速飙升,该怎么排查?

上次面试官问了个问题&#xff1a;应用上线后Cpu使用率飙升如何排查&#xff1f;其实这是个很常见的问题&#xff0c;也非常简单&#xff0c;那既然如此我为什么还要写呢&#xff1f;因为上次回答的时候我忘记将线程PID转换成16进制的命令了。所以我决定再重温一遍这个问题&…

MySQL 死锁了,怎么办?

作者&#xff1a;小林coding提纲如下&#xff1a;正文有个业务主要逻辑就是新增订单、修改订单、查询订单等操作。然后因为订单是不能重复的&#xff0c;所以当时在新增订单的时候做了幂等性校验&#xff0c;做法就是在新增订单记录之前&#xff0c;先通过 select ... for upda…