ThreadLocal的使用及原理解析
# 基本使用
JDK的lang包下提供了ThreadLocal类,我们可以使用它创建一个线程变量,线程变量的作用域仅在于此线程内。<br />用2个示例来展示一下ThreadLocal的用法。
**示例一:**
```java
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
System.out.println(threadLocal.get());
threadLocal.set(1);
System.out.println(threadLocal.get());
threadLocal.remove();
System.out.println(threadLocal.get());
```
输出:
```powershell
null
1
null
```
这个示例展示了ThreadLocal提供的所有方法,ThreadLocal中提供了三个方法,分别是:
- get:获取变量值
- set:设置变量值
- remove:删除变量值
**示例二:**
```java
// 创建一个MyRun类
class MyRun implements Runnable {
// 创建2个线程变量,var1、var2
private ThreadLocal<Integer> var1 = new ThreadLocal<>();
private ThreadLocal<String> var2 = new ThreadLocal<>();
@Override
public void run() {
// 循环调用m方法5次
for (int i = 0; i < 5; i++) {
m();
}
}
public void m() {
// 当前线程名称
String name = Thread.currentThread().getName();
// var1变量从1开始,m每次调用递增1
Integer v = var1.get();
if(v == null) {
var1.set(1);
}else {
var1.set(v + 1);
}
// var2变量 = 线程名 - var1值
var2.set(name + "-" + var1.get());
// 打印
print();
}
public void print() {
String name = Thread.currentThread().getName();
System.out.println(name + ", var1: " + var1.get() + ", var2: " + var2.get());
}
}
```
创建2个线程,执行同一个MyRun:
```java
MyRun myRun = new MyRun();
Thread t1 = new Thread(myRun);
Thread t2 = new Thread(myRun);
t1.start();
t2.start();
```
输出:
```powershell
Thread-0, var1: 1, var2: Thread-0-1
Thread-1, var1: 1, var2: Thread-1-1
Thread-0, var1: 2, var2: Thread-0-2
Thread-1, var1: 2, var2: Thread-1-2
Thread-0, var1: 3, var2: Thread-0-3
Thread-1, var1: 3, var2: Thread-1-3
Thread-0, var1: 4, var2: Thread-0-4
Thread-0, var1: 5, var2: Thread-0-5
Thread-1, var1: 4, var2: Thread-1-4
Thread-1, var1: 5, var2: Thread-1-5
```
示例二展示了ThreadLocal的重要特点:<br />两个线程执行的是同一个MyRun对象,如果var1、var2是普通的成员变量,两个线程访问的将是同一个变量,这将会产生线程安全问题,然而从输出日志看来,t1、t2的var1、var2值其实是独立的,互不影响的。
这是因为var1、var2是ThreadLocal类型,即是线程变量,它是绑定在线程上的,哪个线程来访问这段代码,就从哪个线程上获取var1、var2变量值,线程与线程之间是相互隔离的,因此也不存在线程安全问题。
# 原理解析
ThreadLocal是如何实现这个效果的呢?<br />我们可以从ThreadLocal的源代码中一探究竟。
其中,最关键是get方法,我将get相关的源代码都提取出来如下:
```java
public T get() {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 从当前线程中获取ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
// 从ThreadLocalMap对象中获取当前ThreadLocal对应Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
// 若Entry不为null,返回值
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果获取ThreadLocalMap对象为null则返回默认值
return setInitialValue();
}
// 从指定线程对象获取ThreadLocalMap,也就是Thread中的threadLocals
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 默认值
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);// 如果当前线程的threadLocals不为null,则赋默认值
else
createMap(t, value); // 如果当前线程的threadLocals为null,则新建
return value;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
protected T initialValue() {
return null; // 初始值是null
}
```
从以上这段代码可以看出,ThreadLocal访问的实际上是当前线程的成员变量threadLocals。<br />threadLocals的数据类型是ThreadLocalMap,这是JDK中专门为ThreadLocal设计的数据结构,它本质就是一个键值对类型。<br />ThreadLocalMap的键存储的是当前ThreadLocal对象,值是ThreadLocal对象实际存储的值。<br />当用ThreadLocal对象get方法时,它实际上是从当前线程的threadLocals获取键为当前ThreadLocal对象所对应的值。
画张图来辅助一下理解:<br />
清楚了ThreadLocal的get原理,set和remove方法不需要看源码也能猜出是怎么写的。<br />无非是以ThreadLocal对象为键设置其值或删除键值对。
# ThreadLocal的初始值
上面的介绍,我们看到ThreadLocal的initialValue方法永远都是返回null的:
```java
protected T initialValue() {
return null; // 初始值是null
}
```
如果想要设定ThreadLocal对象的初始值,可以用以下方法:
```java
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(()->1);
System.out.println(threadLocal.get());
```
withInitial方法内实际返回的是一个ThreadLocal子类SuppliedThreadLocal对象。<br />SuppliedThreadLocal重写了ThreadLocal的initialValue方法。
```java
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
@Override
protected T initialValue() {
return supplier.get();
}
}
```
# 获取父线程的ThreadLocal变量
在一些场景下,我们可能需要子线程能获取到父线程的ThreadLocal变量,但使用ThreadLocal是无法获取到的:
```java
public static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
threadLocal.set(1);
System.out.println(threadLocal.get());
Thread childThread = new Thread(() -> System.out.println(threadLocal.get()));
childThread.start();
}
```
输出:
```powershell
1
null
```
使用ThreadLocal的子类**InheritableThreadLocal**可以达到这个效果:
```java
public static ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
public static void main(String[] args) {
threadLocal.set(1);
System.out.println(threadLocal.get());
Thread childThread = new Thread(() -> System.out.println(threadLocal.get()));
childThread.start();
}
```
```powershell
1
1
```
**InheritableThreadLocal是怎么做到的呢?**
我们来分析一下InheritableThreadLocal的源代码。
```java
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的源代码并不多,主要是**覆盖了ThreadLocal的三个方法childValue、getMap、createMap。**<br />childValue方法用于ThreadLocalMap内部使用,我们不打算讲解ThreadLocalMap内部设计,这里可以忽略;<br />ThreadLocal本来getMap、createMap读写的是当前Thread对象的threadLocals变量。<br />而InheritableThreadLocal将其改为了读写当前Thread对象的InheritableThreadLocal变量。
接着我们要从Thread类的源码查找头绪。
Thread类源代码中,我们可以看到有这么2个成员变量:
```java
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
```
**如果是使用ThreadLocal创建线程变量,读写的是Thread对象的threadLocals;**<br />**如果是使用InheritableThreadLocal创建线程变量,读写的是Thread对象的inheritableThreadLocals。**
在Thread类的init方法可以看到(Thread所有构造方法都是调用init方法,这边仅贴出关键部分):
```java
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
```
ThreadLocal.createInheritedMap:
```java
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
```
**如果父级线程的inheritableThreadLocals不为null,那么将父级线程的inheritableThreadLocals赋值到当前线程的inheritableThreadLocals变量。**
总结:当使用InheritableThreadLocal创建线程变量时,父线程读写线程变量实际是写入父线程的inheritableThreadLocals中,在创建子线程时,会将父线程的inheritableThreadLocals复制给子线程的inheritableThreadLocals,子线程操作此线程变量时,也是读写自己线程的inheritableThreadLocals,这就达到了子线程可以获取父线程ThreadLocal的效果。
# 其他要点
- 如果使用了线程池,线程是会被复用的,因此线程的threadLocals和inheritableThreadLocals也会复用,在线程池使用ThreadLocal可能会产生一些问题,需要留意;
- JDK本身提供创建线程池的方法,是不支持获得父级线程的ThreadLocal变量的。
ThreadLocal的使用及原理解析的更多相关文章
- ThreadLocal系列(一)-ThreadLocal的使用及原理解析
ThreadLocal系列之ThreadLocal(源码基于java8) 项目中我们如果想要某个对象在程序运行中的任意位置获取到,就需要借助ThreadLocal来实现,这个对象称作线程的本地变量,下 ...
- ThreadLocal系列(二)-InheritableThreadLocal的使用及原理解析
ThreadLocal系列之InheritableThreadLocal的使用及原理解析(源码基于java8) 上一篇:ThreadLocal系列(一)-ThreadLocal的使用及原理解析 下一篇 ...
- ThreadLocal系列(三)-TransmittableThreadLocal的使用及原理解析
ThreadLocal系列(三)-TransmittableThreadLocal的使用及原理解析 上一篇:ThreadLocal系列(二)-InheritableThreadLocal的使用及原理解 ...
- Java并发包JUC核心原理解析
CS-LogN思维导图:记录CS基础 面试题 开源地址:https://github.com/FISHers6/CS-LogN JUC 分类 线程管理 线程池相关类 Executor.Executor ...
- [原][Docker]特性与原理解析
Docker特性与原理解析 文章假设你已经熟悉了Docker的基本命令和基本知识 首先看看Docker提供了哪些特性: 交互式Shell:Docker可以分配一个虚拟终端并关联到任何容器的标准输入上, ...
- 【算法】(查找你附近的人) GeoHash核心原理解析及代码实现
本文地址 原文地址 分享提纲: 0. 引子 1. 感性认识GeoHash 2. GeoHash算法的步骤 3. GeoHash Base32编码长度与精度 4. GeoHash算法 5. 使用注意点( ...
- Web APi之过滤器执行过程原理解析【二】(十一)
前言 上一节我们详细讲解了过滤器的创建过程以及粗略的介绍了五种过滤器,用此五种过滤器对实现对执行Action方法各个时期的拦截非常重要.这一节我们简单将讲述在Action方法上.控制器上.全局上以及授 ...
- Web APi之过滤器创建过程原理解析【一】(十)
前言 Web API的简单流程就是从请求到执行到Action并最终作出响应,但是在这个过程有一把[筛子],那就是过滤器Filter,在从请求到Action这整个流程中使用Filter来进行相应的处理从 ...
- GeoHash原理解析
GeoHash 核心原理解析 引子 一提到索引,大家脑子里马上浮现出B树索引,因为大量的数据库(如MySQL.oracle.PostgreSQL等)都在使用B树.B树索引本质上是对索引字段 ...
随机推荐
- df空间满,du找不到文件的问题
最近看了一下问题: df -h Filesystem Size Used Avail Use% Mounted on rootfs 271G 267G 2.2G 100% / 根分区满了,du 找不到 ...
- python金牌班第七周周末总结
python金牌班第七周周末总结 面向对象前戏 1.我们在学习面相对像之前有一个推导过程如何将我们之前写的东西,从一串代码转向给对象服务. 2.实例 我们首先模拟了两个物种进行战斗的场景,然后我们发现 ...
- iOS 集成WebRTC相关知识点总结
前言 本文主要是整理了使用WebRTC做音视频通讯时的各知识点及问题点.有理解不足和不到位的地方也欢迎指正. 对于你感兴趣的部分可以选择性观看. WebRTC的初始化 在使用WebRTC的库之前,需要 ...
- xtrabackup增量备份MySQL-5.7操作说明
下载工具 本方法利用xtrabackup二进制包,版本是2.4.26 # 从官网下载二进制包:wget https://downloads.percona.com/downloads/Percona- ...
- Job And Schedule (V8R6C4)
KingbaseES 数据库提供了 kdb_schedule 扩展,使得用户能通过类似oracle job 的方式进行job调用.kdb_schedule 提供了三个Schema :dbms_job ...
- KingbaseES 参数 - ora_statement_level_rollback
参数 ora_statement_level_rollback 控制KingbaseES 是否实现类似oracle 语句级的回滚.当该参数打开时,如果事务操作失败,仅会回滚最后一条操作,避免了全部操作 ...
- 【读书笔记】C#高级编程 第七章 运算符和类型强制转换
(一)运算符 类别 运算符 算术运算符 + - * / % 逻辑运算符 & | ^ ~ && || ! 字符串连接运算符 + 增量和减量运算符 ++ -- 移位运算符 < ...
- python的环境,你再也不用愁-conda
Conda Guide Conda简介 conda是一个包,依赖和环境管理工具,适用于多种语言,如: Python, R, Scala, Java, Javascript, C/ C++, FORTR ...
- 《Java基础——循环语句》
Java基础--循环语句 1. while语句: 规则: 1. 首先计算表达式的值. 2. 若表达式为真,则执行循环语法,直至表达式为假,循环结束. 格式: while(表达式) 语句 ...
- C语言大作业---学生信息管理系统
xxxx信息管理系统 简介 因为大作业规定的踩分项就那么多,为了不浪费时间 + 得分,就写成这样.现在看看,命名不规范,书写风格糟糕,全塞在一个源代码中······ 不过,应付大作业是没问题的 实验报 ...