研二学妹面试字节,竟倒在了ThreadLocal上,这是不要应届生还是不要女生啊?

一、写在开头

    今天和一个之前研二的学妹聊天,聊及她上周面试字节的情况,着实感受到了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();}}@Overridepublic 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并存入valuecreateMap(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. 获取当前线程的ThreadLocalMapThreadLocalMap map = getMap(t);if (map != null) {//3. 获取map中当前ThreadLocal实例为key的值的entryThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")//4. 当前entitiy不为null的话,就返回相应的值valueT result = (T)e.value;return result;}}//5. 若map为null或者entry为null的话通过该方法初始化,并返回该方法返回的valuereturn 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);elsecreateMap(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>(){@Overrideprotected 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. 获取当前线程的ThreadLocalMapThreadLocalMap 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置为nulle.clear();//将该entry的value也置为nullexpungeStaleEntry(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,// 该参数一般默认是 trueboolean 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成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!
在这里插入图片描述

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

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

相关文章

抖音里卖什么最赚钱?4个冷门的高利润商品,还有谁不知道!

哈喽~我的电商月月 做抖音小店的新手朋友&#xff0c;一定很想知道&#xff0c;在抖音里卖什么最赚钱&#xff1f; 很多人都会推荐&#xff0c;日常百货&#xff0c;小风扇&#xff0c;女装&#xff0c;宠物用品等等&#xff0c;这些商品确实很好做&#xff0c;你们可以试试 …

备忘录可以统计字数吗?备忘录里在哪查看字数?

在这个信息爆炸的时代&#xff0c;很多人喜欢使用备忘录app来记录生活中的点点滴滴。备忘录不仅可以帮助我们记事、安排日程&#xff0c;还能提醒我们完成各种任务&#xff0c;是我们日常生活中不可或缺的小助手。 然而&#xff0c;在使用备忘录时&#xff0c;有时我们会遇到需…

不用BookStack的企业都在用什么知识库软件

现如今&#xff0c;越来越多的企业使用知识库软件对企业内部知识进行管理。BookStack作为一款功能强大的开源知识库软件&#xff0c;成为很多企业的首选。但是还是有一部分人群认为BookStack不适合他们的企业那么他们都是在用什么别的知识库软件呢&#xff1f;LookLook同学今天…

《python本机环境多版本切换》-两种方式以及具体使用--venv/pyenv+pycharm测试

阿丹&#xff1a; source myenv/bin/activate 在开发使用rasa的时候发现自己安装的python环境是3.12的&#xff0c;和rasa不兼容&#xff0c;所以实践一下更换多python环境。 使用虚拟环境 在Python中使用虚拟环境来切换Python版本是一个常见的做法&#xff0c;这可以帮助你…

实用篇| huggingface网络不通

之前文章《Transformer原理》中介绍过,Transformers 是由 Hugging Face 开发的一个包&#xff0c;支持加载目前绝大部分的预训练模型。随着 BERT、GPT 等大规模语言模型的兴起&#xff0c;越来越多的公司和研究者采用 Transformers 库来构建应用。 Hugging Face是一家美国公司…

Easy IP + DNAT(服务器NAT转换)

第一章 Easy IP 1.1 一般家庭和企业使用的地址转换方式 直接使用出接口的地址做转换Easy IP适用于小规模居于网中的主机访问Internet的场景如&#xff1a;家庭、小型网吧、小型办公室中&#xff0c;这些地方内部主机不多&#xff0c;出接口可以通过拨号方式获取一个临时公网I…

视频监控汇聚平台LntonCVS通过GB/T28181国标协议实现视频监控平台的级联方案

近年来&#xff0c;随着网络视频监控应用范围的拓展&#xff0c;越来越多的政府部门和跨区域行业单位对视频监控的需求已经不局限于本地联网监控。他们正在探索在原有的本地联网监控基础上&#xff0c;建设省级乃至全国范围内的跨区域监控联网&#xff0c;以全面打造数据共享平…

BUUCTF靶场[Reverse]内涵的文件、新年快乐

[reverse]内涵的文件 文件运行看一下 老规矩&#xff0c;拿到文件先用DIE查有没有壳 没有壳&#xff0c;且是一个32位的文件&#xff0c;用相对应的IDA打开 &#xff0c;有主函数&#xff08;mian&#xff09;&#xff0c;先点开 这里点开&#xff08;mian_0&#xff09;,发现…

【详细介绍WebKit的结构】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

Samtec技术漫谈 | 电动自行车中的传感器和信号传输技术

【摘要/前言】 电动自行车&#xff0c;大家熟悉吗&#xff1f; 今天的话题似乎是可以唤起大家心底骑车的美好回忆&#xff0c;我们也曾骑车探索过大自然和社区&#xff0c;自行车也是我们曾经不可或缺的便捷交通工具。 怀旧思潮的影响&#xff0c;加持科技的进步&#xff0c…

【云原生】Kubernetes中的List-Watch机制详解与容器生命周期

目录 引言 一、List-Watch机制概述 &#xff08;一&#xff09;基本概念 &#xff08;二&#xff09;工作机制 1.List操作 2.Watch操作 &#xff08;三&#xff09;数据流向 1.按模块划分 2.按整体总结 二、Pod生命周期 &#xff08;一&#xff09;生命周期 1.创建…

5款好用的AI写作软件,一键生成高质量文章

在当今信息快速发展的时代&#xff0c;AI写作软件逐渐成为创作者们的得力助手。它们能够凭借先进的技术和算法&#xff0c;一键生成高质量的文章&#xff0c;为创作者们节省大量的创作时间和精力。以下是5款备受好评的AI写作软件&#xff0c;下面在本文中分享给大家&#xff0c…

kettle学习之表的输入输出

需求 把表A里的数据传送到表B中&#xff0c;在此之前&#xff0c;清空表B内的数据 表输入 执行SQL脚本 表输出

一文带你学会如何部署个人博客到云服务器,并进行域名备案与解析!

哈喽&#xff0c;大家好呀&#xff01;这里是码农后端。之前我给大家介绍了如何快速注册一个自己的域名&#xff0c;并创建一台自己的阿里云ECS云服务器。本篇将介绍如何将个人博客部署到云服务器&#xff0c;并进行域名备案与解析。 1、域名备案 注册了域名并购买了云服务器之…

探索自动化办公的新境界:批量操作与智能管理

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、自动化办公的必要性与价值 二、基础操作与自动化脚本 三、Python在自动化办公中的应用…

Meme币总市值突破630亿美元 以太坊ETF获批意味着代币化资产“完全安全”

近日&#xff0c;数字货币市场再次掀起轩然大波。一方面&#xff0c;Meme币总市值突破了630亿美元&#xff0c;令人瞠目结舌&#xff1b;另一方面&#xff0c;以太坊ETF的获批也引发了市场的广泛关注&#xff0c;被视为代币化资产的“完全安全”标志。 Meme币总市值飙升 Meme币…

深圳比创达电子EMC|EMC电磁兼容性行业:挑战与机遇并存

随着电子技术的迅猛发展&#xff0c;电磁兼容性&#xff08;EMC&#xff09;已成为各行各业不可忽视的关键问题。EMC是指设备或系统在其电磁环境中能正常工作且不对该环境中任何事物构成不能承受的电磁骚扰的能力。 一、EMC电磁兼容性行业的现状 EMC电磁兼容性行业作为电子技…

[数据集][目标检测]道路井盖下水道井盖开关闭和检测数据集VOC+YOLO格式407张2类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;407 标注数量(xml文件个数)&#xff1a;407 标注数量(txt文件个数)&#xff1a;407 标注类别…

构建php环境、安装、依赖、nginx配置、ab压力测试命令

目录 php简介 官网php安装包 选择下载稳定版本 &#xff08;建议使用此版本&#xff0c;文章以此版本为例&#xff09; 安装php解析环境 准备工作 安装依赖 zlib-devel 和 libxml2-devel包。 安装扩展工具库 安装 libmcrypt 安装 mhash 安装mcrypt 安装php 选项含…

深入理解一下栈

1、栈&#xff1a;数据结构 为什么 main()方法 最先执行&#xff0c;最后结束&#xff1f; 当然是因为 main()方法入栈啦。 2、栈&#xff1a;栈内存&#xff0c;主管程序的运行&#xff0c;生命周期和现成同步&#xff1b; 线程结束&#xff0c;栈内内存也就释放了&#xff0c…