上一期,讲到了关于线程死锁、用户进程、用户线程的相关知识,不记得的小伙伴可以看看:字节跳动面试官问我:你知道线程死锁吗?用户线程、守护线程的概念与区别了解吗?
这期,我们来聊一聊一个在Java并发编程中很重要的类:ThreadLocal 在多线程应用程序中,对共享变量进行读写的场景是很常见的。如果不使用一定的技术或方案,会引发各种线程安全的问题。常见解决线程安全的方式有synchronized、volatile等方式,但synchronized对性能的开销大,volatile不能保证原子性,所以这里介绍一个 解决多线程间共享变量的线程安全问题 的方法——ThreadLocal

一、ThreadLocal的作用

多线程访问同一个共享变量时特别容易出现并发问题,特别是在多个线程需要对共享变量进行写入时。为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步,如图 1-3 所示

同步的措施一般是加锁,但加锁会在一定程度上增加系统的复杂度以及影响系统的性能。

为了解决多线程间共享变量的线程安全,ThreadLocal应运而生。

当创建一个ThreadLocal变量时,访问这个变量的每个线程都有这个变量的一个本地副本,当多个线程操作这个变量时,实际上就是操作自己本地内存里面的变量,从而避免了线程安全问题。图 1-3 就变成了 图1-4 如图:

二、Threadlocal的使用示例

讲完了理论的东西,我们来通过下面的例子体会下ThreadLocal的神奇之处吧


public class ThreadLocalTest {private static ThreadLocal<String> threadLocal = new ThreadLocal<>();public static void main(String[] args) {Thread thread1 = new Thread(() -> {threadLocal.set("本地变量1");print("thread1");System.out.println("线程1的本地变量的值为:"+threadLocal.get());});Thread thread2 = new Thread(() -> {threadLocal.set("本地变量2");print("thread2");System.out.println("线程2的本地变量的值为:"+threadLocal.get());});thread1.start();thread2.start();}public static void print(String s){System.out.println(s+":"+threadLocal.get());}
}

执行结果如下:


上述代码中,有一个 threadLocal 变量,类型为ThreadLocal ,然后创建了 thread1 和 thread2 ,并分别在两个线程中调用了 threadLocal.set(String str) 方法,然后用 threadLocal.get() 方法去获取threadLocal变量的值。显然,由输出结果可以知道,线程 thread1 中获取到的值就是它给threadLocal设置的值,即为本地变量1;线程 thread2 中获取到的值就是它给threadLocal设置的值,即为本地变量2。这两个线程是访问不到另外一个线程中的threadLocal的值的。

应用讲完了,现在着重来看一下ThreadLocal的实现原理(大厂面试必问~)

1、ThreadLocal 的 set、get方法

首先看下ThreadLocal 相关类的类图结构:

再看一下Thraed里面的成员变量

我们可以发现Thread类中有两个类型为ThreadLocalMap的变量,ThreadLoaclMap是一个定制化的HashMap。

在默认情况下,每个线程中的这两个变量都为null:

 ThreadLocal.ThreadLocalMap threadLocals = null;ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

只有当线程第一次调用ThreadLocal的set方法或get方法时才会创建它们。

 public void set(T value) {
//(1)获取当前线程Thread t = Thread.currentThread();
//(2)将当前线程作为key,去查找对应的线程变量,找到则设置。ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);else
//(3)第一次调用set方法时,就创建当前线程对应的HashMap。createMap(t, value);}

(1)处代码首先获取调用set方法的线程,然后使用当前线程作为参数调用getMap(t) 方法,getMap(Thread t) 方法如下:

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

可以看到,getMap(t) 的作用是获取线程自己的变量 threadLocals ,其类型是ThreadLocalMap

如果getMap(t)的返回值非空,则把value值存放到threadLocals中,即把当前变量值存放入当前线程的成员变量threadLocals中。

threadLocals是一个HashMap结构,其中key就是当前ThreadLocal的实例对象引用,value是通过set方法传递的值。

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

如果getMap(t)返回的是null,则说明是第一次调 set 方法,这时创建 当前线程的threadLocals 变量。 下面来看 createMap(t, value) 干了啥:

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

即创建了一个ThreadLocalMap对象,并将当前线程的threadLocals引用执行它。

再来看看get()方法的代码实现

 public T get() {//(4)获取当前线程Thread t = Thread.currentThread();//(5)获取当前线程的threadLocals变量ThreadLocalMap map = getMap(t);//(6)如果threadLocals不为null,则返回对应的本地变量的值if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}//(7)threadLocals为空时,则初始化当前线程的threadLocals成员变量return setInitialValue();}

(4)处的代码首先获取当前线程实例,如果当前线程的threadLocals不为null,则直接返回当前线程绑定的本地变量;否则执行(7)处代码进行初始化。setInitialValue() 方法如下:

 private T setInitialValue() {//(8)初始化为nullT value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);//(9)如果当前线程的threadLocals变量不为空if (map != null)map.set(this, value);else//(10)为空则创建一个ThreadLocalMap对象,并将当前线程的threadLocals引用执行它。createMap(t, value);return value;}

如果当前线程的threadLocals变量不为空,则设置当前线程的本地变量值为null;否则调用createMap方法创建ThreadLocalMap对象,并将当前线程的threadLocals引用执行它。

总结下:在每个线程里都有 threadLocals 的成员变量,该变量的类型为ThreadLocalMap(实际上可以理解为定制的HashMap),其中key为我们所定义的ThreadLocal变量的this引用,value则为set方法传递的值。每个线程的本地变量存放在线程自己的成员变量threadLocals中,如果当前线程一直不消亡,那么这些本地变量会一直存在,故可能会造成内存溢出,故使用完毕后需要使用 remove() 方法删除threadLocals中的本地变量

2、Threadlocal 不支持继承性

首先看下下面代码:

public class TestThreadLocal {//(1)创建线程变量public static ThreadLocal<String> threadLocal = new ThreadLocal<>();public static void main(String[] args) {//(2)赋值本地变量threadLocal.set("hello world");//(3)启动子线程new Thread(() -> {//(4)子线程输出线程变量的值System.out.println("thread:" + threadLocal.get());}).start();//(5)主线程输出线程变量的值System.out.println("main:" + threadLocal.get());}
}

输出结果如下:


输出结果说明:同一个 ThreadLocal 变量在父线程中被设置值后,在子线程中是获取不到的。
原因是:子线程里面调用get方法时,Thread t = Thread.currentThread() 代码是获取当前线程,当前线程是子线程,而调用set方法给threadLocal赋值的线程是main,两者是不同的线程,故子线程调用get方法取得的threadLocal值为null,main线程调用get方法取得的threadLocal值为“hello world”。

有没有方法让子线程能够访问到父线程中的值?继续往下看啦。

3、lnheritableThreadLocal 类

为了解决让子线程能够访问到父线程中的值的问题,lnheritableThreadLocal 应运而生。lnheritableThreadLocal 继承自 ThreadLocal,并提供了一个新特性:让子线程可以访问在父线程中设置的本地变量值。先来看下lnheritableThreadLocal 的实现:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {//(1)protected T childValue(T parentValue) {return parentValue;}//(2)ThreadLocalMap getMap(Thread t) {return t.inheritableThreadLocals;}//(3)void createMap(Thread t, T firstValue) {t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);}
}

通过查看 InheritableThreadLocal 的源码可知,lnheritableThreadLocal 继承了 ThreadLocal 类并重新了 childValue、getMap、createMap方法。

由(3)处代码可知,InheritableThreadLocal 重写了 createMap 方法,那么当第一次调用 InheritableThreadLocal 实例的set方法时,创建的就是当前线程的inheritableThreadLocals变量的实例而不再是threadLocals了。

由(2)处代码可知,InheritableThreadLocal 重写了 getMap 方法,那么调用InheritableThreadLocal 实例的get方法时,就是获取当前线程的inheritableThreadLocals变量的实例而不再是threadLocals。

那么(1)处代码是如何实现子线程可以访问在父线程中设置的本地变量值的?

这要从创建Thread的代码将起,打开Thread类的默认构造函数:

     public Thread(Runnable target) {init(null, target, "Thread-" + nextThreadNum(), 0);}private void init(ThreadGroup g, Runnable target, String name,long stackSize) {init(g, target, name, stackSize, null, true);}private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,boolean inheritThreadLocals) {......//(4)获取当前线程Thread parent = currentThread();......//(5)如果父线程的inheritableThreadLocals 变量不为nullif (parent.inheritableThreadLocals != null)//(6)设置子线程中的 inheritableThreadLocals 变量this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);......}

由(4)处代码,获取了当前线程(main函数所在的线程,即父线程)

这里可能有同学会有疑问,这里获取到的当前线程为何是父线程?
想一下,当我们new Thread()的时候,是不是在main()方法里执行的,所以当前执行创建Thread代码的线程是main线程,所以(4)处代码中currentThread()方法获取到的就是父线程啦!

由(5)处代码,判断main线程里的inheritableThreadLocals 是否为null,不为null时,则执行代码(6)。

由(6)处代码,我们来看看createInheritedMap()方法:

 static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {return new ThreadLocalMap(parentMap);}

createInheritedMap方法中,使用父线程的inheritableThreadLocals变量作为构造函数创建了一个新的ThreadLocalMap对象,由(6)处:
this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
知道将子线程的inheritableThreadLocals引用指向了这个新创建的ThreadLocalMap对象。

再看看 ThreadLocalMap(parentMap)构造函数:

 private ThreadLocalMap(ThreadLocalMap parentMap) {Entry[] parentTable = parentMap.table;int len = parentTable.length;setThreshold(len);table = new Entry[len];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) {//(7)调用了InheritableThreadLocal类重写的 childValue 方法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变量的值复制到新的ThreadLocalMap对象中,(7)处代码实际上是调用了(1)处代码。

总结一下:InheritableThreadLocal实现子线程可以访问父线程的线程变量的实现原理如下:

  • InheritableThreadLocal通过重写createMap 和 getMap 方法让本地变量保存到了具体线程的inheritableThreadLocal变量中
  • 线程通过调用inheritableThreadLocal实例的setget方法时,就会创建当前线程的inheritableThreadLocal变量
  • 当父线程创建子线程时,构造函数会把父线程中的inheritableThreadLocal变量里面的本地变量值复制一份保存到子线程的inheritableThreadLocal变量里

将最开始的代码作以下修改:

public class TestThreadLocal {//(1)创建线程变量public static InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();public static void main(String[] args) {//(2)赋值本地变量threadLocal.set("hello world");//(3)启动子线程new Thread(() -> {//(4)子线程输出线程变量的值System.out.println("thread:" + threadLocal.get());}).start();//(5)主线程输出线程变量的值System.out.println("main:" + threadLocal.get());}
}

结果就变成了:

很多子线程需要使用父线程中的变量值的场景都可以使用InheritableThreadLocal,是不是很强大呢?

这期就到这里,ThreadLocal、InheritableThreadLocal在Java并发编程中的地位举足轻重,理解了它们的底层实现和应用场景,会让你的大厂面试更有加分项。你们的三连是我创作的最大动力,我们下期见。

解决多线程间共享变量线程安全问题的大杀器——ThreadLocal相关推荐

  1. 获取返回值作为变量_解决多线程间共享变量线程安全问题的大杀器——ThreadLocal...

    微信公众号:Zhongger 我是Zhongger,一个在互联网行业摸鱼写代码的打工人! 关注我,了解更多你不知道的[Java后端]打工技巧.职场经验等- 上一期,讲到了关于线程死锁.用户进程.用户线 ...

  2. 多线程中的线程安全问题

    多线程中的线程安全问题 概述: ​ 多条线程在操作同一个资源的时候发生的数据交叉问题就是线程安全问题 产生原因: ​ 多条线程操作同一个资源 解决思路: ​ 要线程排队解决安全问题,设定权限. 如何设 ...

  3. clodeblocks debug断点调试_Intellij IDEA高阶DEBUG大杀器

    前言 目前工作中由于环境复杂等客观因素,无法在本地启动项目进行 Trouble Shooting,需要打开测试环境的 DEBUG 端口,进行远程调试.为了不影响其他用户同时使用测试环境以及相关系统的正 ...

  4. 并行化-你的高并发大杀器

    作者:咖啡拿铁,现就职于美团点评,后端研发 来自:公众号咖啡拿铁(ID:code_3092860495) 1.前言 想必热爱游戏的同学小时候,都幻想过要是自己要是能像鸣人那样会多重影分身之术,就能一边 ...

  5. 性能提升一个数量级,大杀器来了!| 文内福利

    经过多年的演进,Java语言的功能和性能都在不断地发展和提高,但是冷启动开销较大的问题长期存在,难以从根本上解决.本文先讨论冷启动问题的根本原因,然后介绍一种新近提出的彻底解决Java冷启动问题的技术 ...

  6. Web3+品牌的大杀器:DAO如何实现对传统品牌彻底的降维打击

    01 DAO是品牌的大杀器 在我之前的文章就有预测过,Web3与品牌的结合只有两条路径,一个是Web3会自生长原生品牌发起向现实中的品牌扩大共识,另外一个是Web3是只属于现实中具有某些特性的品牌的沃 ...

  7. Internet Download Manager简直就是下载器中的大杀器

    随着网络的发达,用户的需求越来越多,满足这些需求的电脑软件也越来越多.很多时候,选择一个好的软件,抵得上同类.同系列四五款软件. 新购买的电脑一般都是仅仅安装了最简洁的Windows系统,但是想要实现 ...

  8. bucket sort sample sort 并行_IBM布局AI硬件大杀器:硬软件并行开发、开源模拟AI工具包...

    原标题:IBM布局AI硬件大杀器:硬软件并行开发.开源模拟AI工具包 智东西(公众号:zhidxcom) 编 | 子佩 智东西11月4日消息,为了解决AI对数据.能源和内存资源的巨大需求,IBM一直致 ...

  9. 蚂蚁区块链大杀器首度亮相,速来围观!丨蚂蚁区块链创新大赛站

    进入2019年,区块链加速商用.经过两年多的深度打磨,蚂蚁区块链以大赛的形式向全球的企业.ISV和开发者伸出橄榄枝,探索区块链在更多实体经济中落地的可能性. 此前,性能.隐私保护.安全等成为制约区块链 ...

最新文章

  1. 这道算法题太简单?你忽略了时间复杂度的要求!
  2. IBatis Map时间参数文字格式不匹配!
  3. 栖息在生态办公室,裸心社与USGBC达成战略合作
  4. Tensorflow Lite之编译生成tflite文件
  5. Android面试题详细整理系列(三)
  6. SQL DISTINCT 多字段查询用法
  7. 深入解读MySQL8.0 新特性 :Crash Safe DDL 1
  8. linux 内核同步--理解原子操作、自旋锁、信号量(可睡眠)、读写锁、RCU锁、PER_CPU变量、内存屏障
  9. angularjs的基础知识
  10. html文件变成巨大,巨大的JavaScript HTML5 blob(从大ArrayBuffers)在客户端构建一个巨大的文件...
  11. 内存中的堆和栈(heap stack)
  12. ELK-Metricbeat安装及使用
  13. Date Wed Mar 17 2021 13:52:00 GMT+0800 (中国标准时间) 转标准的日期格式
  14. JAVA设计模式--代理模式(动态)(一)
  15. 对绝对地址0x100000赋值 让程序跳转到绝对地址是0x100000去执行
  16. 0基础光缆/光纤熔接教程
  17. 昔日移动GPU王者,从数据中心、汽车等市场全面杀回来——专访Imagination技术创新副总裁Kristof Beets...
  18. 什么是Mysql的next-key、插入缓冲、二次写、自适应哈希索引和预读
  19. 基于遗传算法的电动汽车有序充放电优化问题
  20. 用java做一个最小的操作系统内核2.

热门文章

  1. 回溯_leetcode.17.电话号码的字母组合
  2. CSS小球下落回弹动画效果
  3. 类定义中class+宏+类名的意义
  4. python西安招聘现状_2018西安招聘求职现状
  5. 100亿数据找出最大的1000个数字的4种方法
  6. 2011 imac 固态_Apple 篇一:iMac 2011升级内存固态实录
  7. Python将txt数据转换为xls(表格)文件,方便后面做数据分析
  8. 营收、净利润均超市场预期,但京东的未来仍存在四大隐忧
  9. 圣诞最酷头像,最全版本来了!!
  10. 三星 Galaxy S8、iPhone 7、华为 Mate9 Pro 基带被黑客攻破