一、写在开头

    今天和一个之前研二的学妹聊天,聊及她上周面试字节的情况,着实感受到了Java后端现在找工作的压力啊,记得在18,19年的时候,研究生计算机专业的学生,背背八股文找个Java开发工作毫无问题,但现在即便你是应届生,问的考题也非常的深入和细节了,只会背八股,没有一定的代码量和项目积累,根本找不到像样的工作,具体聊天内容如下:

既然大厂的面试都拷问到ThreadLocal了,那今天build哥就花点时间也来温习一下这个知识点吧,尽可能整理的细致一点!

二、ThreadLocal简介

2.1 ThreadLocal的作用

处理并发编程的时候,其核心问题是当多个线程去访问共享变量时,因为顺序、资源分配等原因带来了数据的不准确,我们叫这种情况为线程不安全,为了解决线程安全问题,在Java中可以采用Lock、 synchronzed关键字等方式,但这种方式对于没有持有锁的线程来说会阻塞,这样以来在时间性能上就有所损失。

为了解决这个问题,Java的lang包中诞生出了一个类,名为 ThreadLocal,见名知意,它被视为线程的“本地变量”,主要用来存储各线程的私有数据,当多个线程访问同一个ThreadLocal变量时,实际上它们访问的是各自线程本地存储的副本,而不是共享变量本身。因此,每个线程都可以独立地修改自己的副本,而不会影响到其他线程。这种以空间换时间的方式,可以大大的提升处理时间。

2.2 ThreadLocal的使用案例

上面了解了它的特性后,我们来写一个小demo感受一下ThreadLocal的使用。

public class TestService implements Runnable{
// SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
//共享变量
private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd")); public static void main(String[] args) throws InterruptedException {
TestService obj = new TestService();
//循环创建5个线程
for(int i=0 ; i<5; i++){
Thread t = new Thread(obj, ""+i);
Thread.sleep(new Random().nextInt(1000));
t.start();
}
} @Override
public void run() {
System.out.println("Thread:"+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
//formatter pattern is changed here by thread, but it won't reflect to other threads
//设置副本的值
formatter.set(new SimpleDateFormat());
System.out.println("Thread:"+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
}
}

输出:

Thread:0 default Formatter = yyyyMMdd
Thread:1 default Formatter = yyyyMMdd
Thread:2 default Formatter = yyyyMMdd
Thread:1 formatter = yy-M-d ah:mm
Thread:0 formatter = yy-M-d ah:mm
Thread:3 default Formatter = yyyyMMdd
Thread:2 formatter = yy-M-d ah:mm
Thread:3 formatter = yy-M-d ah:mm
Thread:4 default Formatter = yyyyMMdd
Thread:4 formatter = yy-M-d ah:mm

从输出中可以看出,虽然 Thread-0 已经改变了 formatter 的值,但 Thread-1 默认格式化值与初始化值相同并没有被修改,其他线程也一样,这说明每个线程获取ThreadLocal变量值的时候,确访问的时线程本地的副本值。

三、ThreadLocal的实现原理

我们从Thread源码入手,一步步的跟进,去探索ThreadLocal的实现原理。首先,在Thread的源码中,我们看到了这样的两句定义语句:

public class Thread implements Runnable {
//......
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null; //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
//......
}

threadLocals 、inheritableThreadLocals 都是ThreadLocalMap变量,而这个Map我们可以看作是ThreadLocal的定制化HashMap,用来存储线程本地变量的容器,是一个静态内部类,而这两个变量的值初始为null,只有当前线程调用 ThreadLocal 类的 set或get方法时才创建它们,那我们继续去看set/get方法。

【set方法解析】

public void set(T value) {
//1. 获取当前线程实例对象
Thread t = Thread.currentThread(); //2. 通过当前线程实例获取到ThreadLocalMap对象
ThreadLocalMap map = getMap(t); if (map != null)
//3. 如果Map不为null,则以当前ThreadLocal实例为key,值为value进行存入
map.set(this, value);
else
//4.map为null,则新建ThreadLocalMap并存入value
createMap(t, value);
}

在ThreadLocal的set方法中通过getMap()方法去获取当前线程的ThreadLocalMap对象,并对获取到的map进行判断,我们跟如到getMap方法中去,发现其实里面返回的是初始化定义的threadLocals变量。

ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

在threadLocals没有被调用初始化方法重新赋值的时候,它为null(不为null时,直接set进行赋值,当前ThreadLocal实例为key,值为value),set方法中会去调用createMap(t,value)进行处理,我们继续跟入这个方法的源码去看看:

void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

我们可以看到,在这个方法内部,会去新构造一个ThreadLocalMap的实例,并将value值初始化进去,并赋给threadLocals。

看完了set方法的底层实现我们知道:

  1. 最终变量存储的位置在ThreadLocalMap里,ThreadLocal可以视为这个Map的封装;
  2. 无论如何最终threadLocals存储的数据都是以线程为key,对应的局部变量为值得映射表;
  3. 因为映射表的原因,确保了每个线程的局部变量都时独立的。

【get方法解析】

看完了set的源码,我们继续来看看get方法的底层实现吧,既然有存(set)就有取(get),get 方法提供的就是获取当前线程中 ThreadLocal 的变量值的功能!

public T get() {
//1. 获取当前线程的实例对象
Thread t = Thread.currentThread(); //2. 获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//3. 获取map中当前ThreadLocal实例为key的值的entry
ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) {
@SuppressWarnings("unchecked")
//4. 当前entitiy不为null的话,就返回相应的值value
T result = (T)e.value;
return result;
}
}
//5. 若map为null或者entry为null的话通过该方法初始化,并返回该方法返回的value
return setInitialValue();
}

我们上面提到了线程的变量值是和线程的ThreadLocal有映射关系的,所以这里将当前线程的ThreadLocal作为key去map中获取值,若map为null或者entry为null的话通过该方法初始化,并返回该方法返回的value,我们去看看setInitialValue的实现:

private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}

这个方法里的实现和set几乎一模一样,这里调用了一个protected访问修饰符的方法initialValue(),这个方法可以被子类重写。

我们在2.2使用案例中写道的ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd"));这是在Java8中的写法,等价于:

private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue(){
return new SimpleDateFormat("yyyyMMdd");
}
};

setInitialValue 方法的目的是确保每个线程在第一次尝试访问其 ThreadLocal 变量时都有一个合适的值。

3.1 ThreadLocalMap

上面我们也说了,ThreadLocalMap是ThreadLocal的静态内部类,而每个线程独立的变量副本存储也是在这个Map中,它是一个定制的哈希表,底层维护了一个Entry 类型的数组类型的数组 table,它的内部提供了set、remove、getEntry等方法。

Entry静态内部类

这个Entry又是ThreadLocalMap的一个静态内部类,我们看一下它的源码:

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

Entry 继承了弱引用 WeakReference<ThreadLocal<?>>,它的 value 字段用于存储与特定 ThreadLocal 对象关联的值,key 为弱引用,意味着当 ThreadLocal 外部强引用被置为 null(ThreadLocalInstance=null)时,根据可达性分析,ThreadLocal 实例此时没有任何一条链路引用它,所以系统 GC 的时候 ThreadLocal 会被回收。这种操作看似利用垃圾回收器节省了内存空间,实则存在一个风险,也就是我们下面要说的内存泄露问题!

只具有弱引用的对象,拥有更为短暂的生命周期,在GC线程扫描到它所在的内存区域的时候,一旦发现了只有弱引用的对象的时候,不管内存够不够用都会将其回收掉

四、ThreadLocal内存泄漏问题

4.1 内存泄漏的原因

如果非要问ThreadLocal有什么缺点的话,那就是使用不当的时候,会带来内存泄漏问题。

内存泄漏 是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

根据3.1中的分析,我们知道ThreadLocalMap中的使用的key是ThreadLocal的弱引用,Value为强引用,如果ThreadLocal没有被强引用的话,key会被GC掉,而value依旧存在,若我们采用任何措施的前提下,线程一直运行,那这些value值就会一直存在,过多的占用内存,导致内存泄漏!

4.2 如何解决内存泄漏

如何解决内存泄漏呢,只需要记得在使用完 ThreadLocal 中存储的内容后将它 remove 掉就可以了。

//ThreadLocal提供的清理方法
public void remove() {
//1. 获取当前线程的ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//2. 从map中删除以当前ThreadLocal实例为key的键值对
m.remove(this);
}
/**
* ThreadLocalMap中的remove方法
*/
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.ThreadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
//将entry的key置为null
e.clear();
//将该entry的value也置为null
expungeStaleEntry(i);
return;
}
}
}

除此之外,我们还可以使用Java 8引入的InheritableThreadLocal来替代ThreadLocal,它可以在子线程中自动继承父线程的线程局部变量值,从而避免在创建新线程时重复设置值的问题。但是同样需要注意及时清理资源以避免内存泄漏。

五、线程间局部变量传值问题

上面我们提到的Java8中引入的InheritableThreadLocal类,这是实现父子线程间局部变量传值的关键!

InheritableThreadLocal存在于java.lang包中是ThreadLocal的扩展,它有一个特性,那就是当创建一个新的线程时,如果父线程中有一个 InheritableThreadLocal 变量,那么子线程将会继承这个变量的值。这意味着子线程可以访问其父线程为此类变量设置的值。我们写一个小demo感受一下!

public class TestService{
// 创建一个 InheritableThreadLocal 变量
private static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>(); public static void main(String[] args) {
// 在主线程中设置值
inheritableThreadLocal.set("这是父线程的值"); System.out.println("父线程中的值: " + inheritableThreadLocal.get()); // 创建一个子线程
Thread childThread = new Thread(() -> {
// 在子线程中尝试获取值,由于使用了 InheritableThreadLocal,这里会获取到父线程中设置的值
System.out.println("子线程中的值: " + inheritableThreadLocal.get());
}); // 启动子线程
childThread.start(); // 等待子线程执行完成
try {
childThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 主线程结束时清除值,防止潜在的内存泄漏
inheritableThreadLocal.remove();
}
}

输出:

父线程中的值: 这是父线程的值
子线程中的值: 这是父线程的值

输出不出所料,在子线程中获取的其实是父线程设置的inheritableThreadLocal值。

5.1 父子线程局部变量传值的实现原理

我们看到上面的输出后,应该思考这样的一个问题:子线程是怎么拿到父线程的inheritableThreadLocal值得呢?其实要从子线程的初始化开始说起,在线程Thread的内部,有着这样的一个初始化方法:

private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
// 该参数一般默认是 true
boolean inheritThreadLocals) {
// 省略大部分代码
Thread parent = currentThread(); // 复制父线程的 inheritableThreadLocals 属性,实现父子线程局部变量共享
if (inheritThreadLocals && parent.inheritableThreadLocals != null) {
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
// 省略部分代码
}

在这里将父线程的inheritableThreadLocals赋值了进来,我们跟入createInheritedMap方法中继续解析:

// 返回一个ThreadLocalMap,传值为父线程的
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
//ThreadLoaclMap构建的过程中会调用该构造方法
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
// 一个个复制父线程 ThreadLocalMap 中的数据
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
// childValue 方法调用的是 InheritableThreadLocal#childValue(T parentValue)
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}

在这个构造方法中,我们终于看到了InheritableThreadLocal的身影,childValue()方法就是其中的一个方法,用来给子线程赋父线程的inheritableThreadLocals值;其实InheritableThreadLocal的源码非常非常的简单,大部分的实现都取自父类ThreadLocal。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    protected T childValue(T parentValue) {
return parentValue;
} ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
} void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}

六、总结

OK,基于学妹在字节面试的考点,我们又梳理了一遍ThreadLocal,这个类大家还是要好好学一学的,毕竟在日后的工作中,我们肯定会使用到,譬如用它来保存用户登录信息,这样在同一个线程中的任何地方都可以获取到登录信息;用于保存事务上下文,这样在同一个线程中的任何地方都可以获取到事务上下文等等。

七、结尾彩蛋

如果本篇博客对您有一定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥!

如果您想与Build哥的关系更近一步,还可以关注“JavaBuild888”,在这里除了看到《Java成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!

研二学妹面试字节,竟倒在了ThreadLocal上,这是不要应届生还是不要女生啊?的更多相关文章

  1. 此文记录了我从研二下学期到研三上学期的找工历程,包括百度、腾讯、网易、移动、电信、华为、中兴、IBM八家企业的面试总结和心得--转

    感谢电子通讯工程的研究生学长为大家整理了这么全面的求职总结,希望进入通信公司和互联网公司做非技术类岗位的学弟学妹们千万不要错过哦~ ---------------------------原文分割线-- ...

  2. 靠这些秋招秘笈,齐姐的学妹今年已经拿到了 8 个offer!

    小齐说: 现在秋招进行时,正在找工作的小伙伴进度都怎么样了呀? 今天这篇文章是我武大的学妹今年秋招的经验分享,庆妹去年才决定转行,现在已手握 N+ 个 offer - 这篇文章干货满满,庆妹对每一块面 ...

  3. 5年Android程序员面试字节跳动两轮后被完虐,请查收给你的面试指南

    大家应该看过很多分享面试成功的经验,但根据幸存者偏差的理论,也许多看看别人面试失败在哪里,对自己才更有帮助. 最近跟一个朋友聊天,他准备了几个月,刚刚参加完字节跳动面试,第二面结束后,嗯,挂了- 所以 ...

  4. 【北邮人论坛帖子备份】【FAQ】给今年国企求职学弟学妹的一点建议

    目录 主要内容 补充--[心得][校招]面向国企秋招的一些体会 补充--[心得]回馈论坛,某toca的小弟金融类央企秋招过程中的一点点 主要内容 时间:Fri Feb 19 23:00:33 2021 ...

  5. 我的屌丝giser成长记-研二篇

    之前有提到过的,本来按照计划中,研一结束就该去深圳中科院研究所实习的,之前跟里面师兄说好了的,奈何导师又接到一个新的科研研究项目,跟学院的几个其他老师一起合作的,主要是关于土地流转系统,而且是一个挺大 ...

  6. 给学妹的 Java 学习路线

    大家好,这篇文章主要是讲解下如何自学 Java,这个问题有很多粉丝私信问过,今天又有直系学妹问我如何学习 Java? 我就以我的经历,总结下分享给大家,有不当指出或者有更好的方法建议也欢迎留言指出,大 ...

  7. 给大一的学弟学妹们培训java web的后台开发讨论班计划

    蓝旭工作室5月大一讨论班课程计划   课时 讨论班性质 讨论班名称 主要内容 主讲人   第一讲 先导课 后台开发工具的使用与MySQL数据库基础 后台开发工具的基本使用方法与工程的创建,MySQL数 ...

  8. 11th 回忆整个学期——告学弟学妹

    告诉后来的学弟学妹,不要因为艰难而却步,坚持下去才知道,山的对面是什么.很多东西或许一开始看起来是无用,甚至无意义的,但是努力去做,你才知道价值所在.不要等一切结束了,才懂得自己错过了什么.

  9. AOJ.720 丢失的学妹

    缺失的学妹 考察点 STL MAP Time Mem Len Lang 3.81s 39.1MB 0.68K G++ 题意分析 给出妹子学号的个数n,给出n个学号,和n-1个学号,求在n学号中那个没有 ...

  10. hpuoj--校赛--与学妹滑雪(最短路+精度判断)

    问题 G: 感恩节KK专场--与学妹滑雪 时间限制: 1 Sec  内存限制: 128 MB 提交: 284  解决: 25 [提交][状态][讨论版] 题目描述 这周下的雪好大好大,不过这正和KK学 ...

随机推荐

  1. Kubernetes 的 NameSpace 无法删除应该怎么办?

    概述 有时候我们操作不规范,或者删除的先后顺序有问题,或者某项关键服务没有启动,导致 Kubernetes 经常会出现无法删除 NameSpace 的情况.这种情况下我们应该怎么办? 规范删除流程 其 ...

  2. 动态规划(六)——树形dp

    树形dp,又称树状dp,即在树上进行的dp,在设计动态规划算法时,一般就以节点从深到浅(子树从小到大)的顺序作为dp的"阶段",dp的状态表示中,第一维通常是节点编号(代表以该节点 ...

  3. css 文字溢出省略号

    前言 css 文字溢出后显示省略号,这是一个非常常规的操作,但是你会发现在网上很多给出的例子两行之后显示省略号,却没有用. 这是为什么呢?please look follow. 正文 在一行省略的: ...

  4. 实验k8s ————— k8s 搭建[一]

    前言 以前学习k8s记录的.这里简单整理一下搭建,当时是我们学习环境的搭建,正式环境得专门的运维人员来,毕竟人家考虑的东西不一样. 正文 这里用kubeadm进行搭建,更加详细信息,在这里: http ...

  5. List拖拽功能的实现

    概述   如何在HarmonyOS应用中实现一个可拖拽的列表组件,通过这个组件,用户可以拖动列表中的项并将其放置在新的位置,实现列表的动态排序.   核心功能   列表初始化:创建并填充列表数据. 拖 ...

  6. 力扣1132(MySQL)-报告的记录Ⅱ(中等)

    题目: 编写一段 SQL 来查找:在被报告为垃圾广告的帖子中,被移除的帖子的每日平均占比,四舍五入到小数点后 2 位. Actions 表: Removals 表: Result 表: 2019-07 ...

  7. Hologres如何支持超高基数UV计算(基于roaringbitmap实现)

    简介: 本文将会介绍Hologres基于roaringbitmap实现超高基数的UV计算 RoaringBitmap是一种压缩位图索引,RoaringBitmap自身的数据压缩和去重特性十分适合对于大 ...

  8. 如何开发 Node.js Native Add-on?

    简介: 来一起为 Node.js 的 add-on 生态做贡献吧~ 作者 | 吴成忠(昭朗) 这篇文章是由 Chengzhong Wu (@legendecas),Gabriel Schulhof ( ...

  9. [FAQ] 如何从 svg 字符串创建 SVGElement

      HTML 部分: <div style="display: none" id="svgCon"></div> JS 部分: cons ...

  10. Azure 无服务器 Function 函数计算服务 dotnet core 3.1 创建和部署入门

    本文用的是 世纪互联 的 Azure.cn 版本,这个版本因为是在国内,所以网速会快超级超级多.使用 世纪互联 的版本需要一块钱哦,用一块钱就能进入一个月的免费试用.本文主要告诉小伙伴如何使用 Azu ...