一文让你彻底掌握ThreadLocal
本文分享自华为云社区《【高并发】一文带你彻底搞懂ThreadLocal》,作者: 冰 河。
我们都知道,在多线程环境下访问同一个共享变量,可能会出现线程安全的问题,为了保证线程安全,我们往往会在访问这个共享变量的时候加锁,以达到同步的效果,如下图所示。
对共享变量加锁虽然能够保证线程的安全,但是却增加了开发人员对锁的使用技能,如果锁使用不当,则会导致死锁的问题。而ThreadLocal能够做到在创建变量后,每个线程对变量访问时访问的是线程自己的本地变量。
什么是ThreadLocal?
ThreadLocal是JDK提供的,支持线程本地变量。也就是说,如果我们创建了一个ThreadLocal变量,则访问这个变量的每个线程都会有这个变量的一个本地副本。如果多个线程同时对这个变量进行读写操作时,实际上操作的是线程自己本地内存中的变量,从而避免了线程安全的问题。
ThreadLocal使用示例
例如,我们使用ThreadLocal保存并打印相关的变量信息,程序如下所示。
public class ThreadLocalTest { private static ThreadLocal<String> threadLocal = new ThreadLocal<String>(); public static void main(String[] args){ //创建第一个线程 Thread threadA = new Thread(()->{ threadLocal.set("ThreadA:" + Thread.currentThread().getName()); System.out.println("线程A本地变量中的值为:" + threadLocal.get()); }); //创建第二个线程 Thread threadB = new Thread(()->{ threadLocal.set("ThreadB:" + Thread.currentThread().getName()); System.out.println("线程B本地变量中的值为:" + threadLocal.get()); }); //启动线程A和线程B threadA.start(); threadB.start(); } }
运行程序,打印的结果信息如下所示。
线程A本地变量中的值为:ThreadA:Thread-0 线程B本地变量中的值为:ThreadB:Thread-1
此时,我们为线程A增加删除ThreadLocal中的变量的操作,如下所示。
public class ThreadLocalTest { private static ThreadLocal<String> threadLocal = new ThreadLocal<String>(); public static void main(String[] args){ //创建第一个线程 Thread threadA = new Thread(()->{ threadLocal.set("ThreadA:" + Thread.currentThread().getName()); System.out.println("线程A本地变量中的值为:" + threadLocal.get()); threadLocal.remove(); System.out.println("线程A删除本地变量后ThreadLocal中的值为:" + threadLocal.get()); }); //创建第二个线程 Thread threadB = new Thread(()->{ threadLocal.set("ThreadB:" + Thread.currentThread().getName()); System.out.println("线程B本地变量中的值为:" + threadLocal.get()); System.out.println("线程B没有删除本地变量:" + threadLocal.get()); }); //启动线程A和线程B threadA.start(); threadB.start(); } }
此时的运行结果如下所示。
线程A本地变量中的值为:ThreadA:Thread-0 线程B本地变量中的值为:ThreadB:Thread-1 线程B没有删除本地变量:ThreadB:Thread-1 线程A删除本地变量后ThreadLocal中的值为:null
通过上述程序我们可以看出,线程A和线程B存储在ThreadLocal中的变量互不干扰,线程A存储的变量只能由线程A访问,线程B存储的变量只能由线程B访问。
ThreadLocal原理
首先,我们看下Thread类的源码,如下所示。
public class Thread implements Runnable { /***********省略N行代码*************/ ThreadLocal.ThreadLocalMap threadLocals = null; ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; /***********省略N行代码*************/ }
由Thread类的源码可以看出,在ThreadLocal类中存在成员变量threadLocals和inheritableThreadLocals,这两个成员变量都是ThreadLocalMap类型的变量,而且二者的初始值都为null。只有当前线程第一次调用ThreadLocal的set()方法或者get()方法时才会实例化变量。
这里需要注意的是:每个线程的本地变量不是存放在ThreadLocal实例里面的,而是存放在调用线程的threadLocals变量里面的。也就是说,调用ThreadLocal的set()方法存储的本地变量是存放在具体线程的内存空间中的,而ThreadLocal类只是提供了set()和get()方法来存储和读取本地变量的值,当调用ThreadLocal类的set()方法时,把要存储的值放入调用线程的threadLocals中存储起来,当调用ThreadLocal类的get()方法时,从当前线程的threadLocals变量中将存储的值取出来。
接下来,我们分析下ThreadLocal类的set()、get()和remove()方法的实现逻辑。
set()方法
set()方法的源代码如下所示。
public void set(T value) { //获取当前线程 Thread t = Thread.currentThread(); //以当前线程为Key,获取ThreadLocalMap对象 ThreadLocalMap map = getMap(t); //获取的ThreadLocalMap对象不为空 if (map != null) //设置value的值 map.set(this, value); else //获取的ThreadLocalMap对象为空,创建Thread类中的threadLocals变量 createMap(t, value); }
在set()方法中,首先获取调用set()方法的线程,接下来,使用当前线程作为Key调用getMap(t)方法来获取ThreadLocalMap对象,getMap(Thread t)的方法源码如下所示。
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
可以看到,getMap(Thread t)方法获取的是线程变量自身的threadLocals成员变量。
在set()方法中,如果调用getMap(t)方法返回的对象不为空,则把value值设置到Thread类的threadLocals成员变量中,而传递的key为当前ThreadLocal的this对象,value就是通过set()方法传递的值。
如果调用getMap(t)方法返回的对象为空,则程序调用createMap(t, value)方法来实例化Thread类的threadLocals成员变量。
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
也就是创建当前线程的threadLocals变量。
get()方法
get()方法的源代码如下所示。
public T get() { //获取当前线程 Thread t = Thread.currentThread(); //获取当前线程的threadLocals成员变量 ThreadLocalMap map = getMap(t); //获取的threadLocals变量不为空 if (map != null) { //返回本地变量对应的值 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } //初始化threadLocals成员变量的值 return setInitialValue(); }
通过当前线程来获取threadLocals成员变量,如果threadLocals成员变量不为空,则直接返回当前线程绑定的本地变量,否则调用setInitialValue()方法初始化threadLocals成员变量的值。
private T setInitialValue() { //调用初始化Value的方法 T value = initialValue(); Thread t = Thread.currentThread(); //根据当前线程获取threadLocals成员变量 ThreadLocalMap map = getMap(t); if (map != null) //threadLocals不为空,则设置value值 map.set(this, value); else //threadLocals为空,创建threadLocals变量 createMap(t, value); return value; }
其中,initialValue()方法的源码如下所示。
protected T initialValue() { return null; }
通过initialValue()方法的源码可以看出,这个方法可以由子类覆写,在ThreadLocal类中,这个方法直接返回null。
remove()方法
remove()方法的源代码如下所示。
public void remove() { //根据当前线程获取threadLocals成员变量 ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) //threadLocals成员变量不为空,则移除value值 m.remove(this); }
remove()方法的实现比较简单,首先根据当前线程获取threadLocals成员变量,不为空,则直接移除value的值。
注意:如果调用线程一致不终止,则本地变量会一直存放在调用线程的threadLocals成员变量中,所以,如果不需要使用本地变量时,可以通过调用ThreadLocal的remove()方法,将本地变量从当前线程的threadLocals成员变量中删除,以免出现内存溢出的问题。
ThreadLocal变量不具有传递性
使用ThreadLocal存储本地变量不具有传递性,也就是说,同一个ThreadLocal在父线程中设置值后,在子线程中是无法获取到这个值的,这个现象说明ThreadLocal中存储的本地变量不具有传递性。
接下来,我们来看一段代码,如下所示。
public class ThreadLocalTest { private static ThreadLocal<String> threadLocal = new ThreadLocal<String>(); public static void main(String[] args){ //在主线程中设置值 threadLocal.set("ThreadLocalTest"); //在子线程中获取值 Thread thread = new Thread(new Runnable() { @Override public void run() { System.out.println("子线程获取值:" + threadLocal.get()); } }); //启动子线程 thread.start(); //在主线程中获取值 System.out.println("主线程获取值:" + threadLocal.get()); } }
运行这段代码输出的结果信息如下所示。
主线程获取值:ThreadLocalTest 子线程获取值:null
通过上述程序,我们可以看出在主线程中向ThreadLocal设置值后,在子线程中是无法获取到这个值的。那有没有办法在子线程中获取到主线程设置的值呢?此时,我们可以使用InheritableThreadLocal来解决这个问题。
InheritableThreadLocal使用示例
InheritableThreadLocal类继承自ThreadLocal类,它能够让子线程访问到在父线程中设置的本地变量的值,例如,我们将ThreadLocalTest类中的threadLocal静态变量改写成InheritableThreadLocal类的实例,如下所示。
public class ThreadLocalTest { private static ThreadLocal<String> threadLocal = new InheritableThreadLocal<String>(); public static void main(String[] args){ //在主线程中设置值 threadLocal.set("ThreadLocalTest"); //在子线程中获取值 Thread thread = new Thread(new Runnable() { @Override public void run() { System.out.println("子线程获取值:" + threadLocal.get()); } }); //启动子线程 thread.start(); //在主线程中获取值 System.out.println("主线程获取值:" + threadLocal.get()); } }
此时,运行程序输出的结果信息如下所示。
主线程获取值:ThreadLocalTest 子线程获取值:ThreadLocalTest
可以看到,使用InheritableThreadLocal类存储本地变量时,子线程能够获取到父线程中设置的本地变量。
InheritableThreadLocal原理
首先,我们来看下InheritableThreadLocal类的源码,如下所示。
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); } }
由InheritableThreadLocal类的源代码可知,InheritableThreadLocal类继承自ThreadLocal类,并且重写了ThreadLocal类的childValue()方法、getMap()方法和createMap()方法。也就是说,当调用ThreadLocal的set()方法时,创建的是当前Thread线程的inheritableThreadLocals成员变量而不再是threadLocals成员变量。
这里,我们需要思考一个问题:InheritableThreadLocal类的childValue()方法是何时被调用的呢? 这就需要我们来看下Thread类的构造方法了,如下所示。
public Thread() { init(null, null, "Thread-" + nextThreadNum(), 0); } public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); } Thread(Runnable target, AccessControlContext acc) { init(null, target, "Thread-" + nextThreadNum(), 0, acc, false); } public Thread(ThreadGroup group, Runnable target) { init(group, target, "Thread-" + nextThreadNum(), 0); } public Thread(String name) { init(null, null, name, 0); } public Thread(ThreadGroup group, String name) { init(group, null, name, 0); } public Thread(Runnable target, String name) { init(null, target, name, 0); } public Thread(ThreadGroup group, Runnable target, String name) { init(group, target, name, 0); } public Thread(ThreadGroup group, Runnable target, String name, long stackSize) { init(group, target, name, stackSize); }
可以看到,Thread类的构造方法最终调用的是init()方法,那我们就来看下init()方法,如下所示。
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { /************省略部分源码************/ if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); /* Stash the specified stack size in case the VM cares */ this.stackSize = stackSize; /* Set thread ID */ tid = nextThreadID(); }
可以看到,在init()方法中会判断传递的inheritThreadLocals变量是否为true,同时父线程中的inheritableThreadLocals是否为null,如果传递的inheritThreadLocals变量为true,同时,父线程中的inheritableThreadLocals不为null,则调用ThreadLocal类的createInheritedMap()方法。
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { return new ThreadLocalMap(parentMap); }
在createInheritedMap()中,使用父线程的inheritableThreadLocals变量作为参数创建新的ThreadLocalMap对象。然后在Thread类的init()方法中会将这个ThreadLocalMap对象赋值给子线程的inheritableThreadLocals成员变量。
接下来,我们来看看ThreadLocalMap的构造函数都干了啥,如下所示。
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) { //调用重写的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++; } } } }
在ThreadLocalMap的构造函数中,调用了InheritableThreadLocal类重写的childValue()方法。而InheritableThreadLocal类通过重写getMap()方法和createMap()方法,让本地变量保存到了Thread线程的inheritableThreadLocals变量中,线程通过InheritableThreadLocal类的set()方法和get()方法设置变量时,就会创建当前线程的inheritableThreadLocals变量。此时,如果父线程创建子线程,在Thread类的构造函数中会把父线程中的inheritableThreadLocals变量里面的本地变量复制一份保存到子线程的inheritableThreadLocals变量中。
一文让你彻底掌握ThreadLocal的更多相关文章
- ThreadLocal的正确用法
用法一:在关联数据类中创建private static ThreadLocalThreaLocal的JDK文档中说明:ThreadLocal instances are typically priva ...
- 一文搞懂 ThreadLocal 原理
当多线程访问共享可变数据时,涉及到线程间同步的问题,并不是所有时候,都要用到共享数据,所以就需要线程封闭出场了. 数据都被封闭在各自的线程之中,就不需要同步,这种通过将数据封闭在线程中而避免使用同步的 ...
- 【Java EE 学习 19】【使用过滤器实现全站压缩】【使用ThreadLocal模式解决跨DAO事务回滚问题】
一.使用过滤器实现全站压缩 1.目标:对网站的所有JSP页面进行页面压缩,减少用户流量的使用.但是对图片和视频不进行压缩,因为图片和视频的压缩率很小,而且处理所需要的服务器资源很大. 2.实现原理: ...
- java多线程学习-ThreadLocal
为了凑字,把oracle文档里介绍ThreadLocal抄过来 public class ThreadLocal<T> extends Object This class provides ...
- Java多线程之 ThreadLocal
一.什么是ThreadLocal? 顾名思义它是local variable(线程局部变量).它的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是每一个线程都可以独立地改变自己的副 ...
- Java多线程学习之ThreadLocal源码分析
0.概述 ThreadLocal,即线程本地变量,是一个以ThreadLocal对象为键.任意对象为值的存储结构.它可以将变量绑定到特定的线程上,使每个线程都拥有改变量的一个拷贝,各线程相同变量间互不 ...
- ThreadLocal 原理和使用场景分析
ThreadLocal 不知道大家有没有用过,但至少听说过,今天主要记录一下 ThreadLocal 的原理和使用场景. 使用场景 直接定位到 ThreadLocal 的源码,可以看到源码注释中有很清 ...
- ThreadLocal就是这么简单
前言 今天要研究的是ThreadLocal,这个我在一年前学习JavaWeb基础的时候接触过一次,当时在baidu搜出来的第一篇博文ThreadLocal,在评论下很多开发者认为那博主理解错误,给出了 ...
- 一文带你认识Spring事务
前言 只有光头才能变强. 文本已收录至我的GitHub仓库,欢迎Star:https://github.com/ZhongFuCheng3y/3y Spring事务管理我相信大家都用得很多,但可能仅仅 ...
- 最新阿里Java技术面试题,看这一文就够了!
金三银四跳槽季即将到来,作为 Java 开发者你开始刷面试题了吗?别急,小编整理了阿里技术面试题,看这一文就够了! 阿里面试题目目录 技术一面(基础面试题目) 技术二面(技术深度.技术原理) 项目实战 ...
随机推荐
- vscode/sublime 语法高亮定义和代码段的区别
vscode插件数据格式基于json,sublime插件数据格式基于xml.sublime插件的官方文档说的不清楚,相关教程也很难找,遇到的一些坑记录一下 语法定义文件对比 同样使用TextMate定 ...
- idea 连接远程 docker 并部署项目到 docker
目录 idea 连接远程 docker 1. 安装 docker 插件 2. 登录远程服务器,修改docker配置 3. 添加云服务器防火墙规则 4. idea 配置连接 docker 部署项目到 d ...
- 2021入门Webpack,看这篇就够了:Webpack.config.js 解析
这是一个webpack配置说明 本文是发布在github上webpack-demo的README文件内容. 主要对webpack.config.js每一条的注释说明. 希望浏览效果更佳,可以点击文章最 ...
- windows 下终止nginx 进程 重新启动nginx
进入cmd 输入一下命令 删除nginx所有进程 taskkill /f /t /im nginx.exe
- Ubuntu 20.04 开启局域网唤醒(WoL)
打开主板相关设置 创建 systemd 自启动设置文件 vim /etc/systemd/system/wol@.service 放入以下内容: [Unit] Description=Wake-on- ...
- Go语言数组与切片学习总结
一.数组 数组的定义:相同类型的数据集合 go语言中数组的索引从0开始 没有赋值的数值型数组,默认值为0 数组一旦被创建,它的大小就是不可改变的 (1)声明数组与打印 var 变量名 [大小]变量类型 ...
- JTAG串链
- Redis本地安装以及使用(详细教程)
Redis 安装 Windows 下载安装 Redis默认端口:6379 整个过程如下: 1.下载连接 https://github.com/tporadowski/redis/releases Re ...
- [ABC246B] Get Closer
section> Problem Statement From the point $(0,0)$ in a two-dimensional plane, let us move the dis ...
- 一款开源免费美观的WinForm UI控件库 - ReaLTaiizor
前言 今天推荐一款基于MIT license开源.免费.美观的.NET WinForm UI控件库:ReaLTaiizor. 什么是WinForm? WinForm是一个传统的桌面应用程序框架,它基于 ...