当前位置:网站首页>ThreadLocal详解
ThreadLocal详解
2022-06-27 01:31:00 【ZNineSun】
1.ThreadLocal简介
在 java 线程中,每个线程都有一个 ThreadLocalMap 实例变量(如果不使用 ThreadLocal,不会创建这个 Map,一个线程第一次访问某个 ThreadLocal 变量时,才会创建)。
ThreadLocal很容易让人望文生义,想当然地认为是一个“本地线程”。其实,ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。
它主要由四个方法组成initialValue(),get(),set(T),remove(),其中值得注意的是initialValue(),该方法是一个protected的方法,显然是为了子类重写而特意实现的。该方法返回当前线程在该线程局部变量的初始值,这个方法是一个延迟调用方法,在一个线程第1次调用get()时才执行,并且仅执行1次(即:最多在每次访问线程来获得每个线程局部变量时调用此方法一次,即线程第一次使用get()方法访问变量的时候。如果线程先于get方法调用set(T)方法,则不会在线程中再调用initialValue方法)。ThreadLocal中的缺省实现直接返回一个null:
该 Map 是使用线性探测的方式解决 hash 冲突的问题,如果没有找到空闲的 slot,就不断往后尝试,直到找到一个空闲的位置,插入 entry,这种方式在经常遇到 hash 冲突时,影响效率。
下面我们就具体探讨一下ThreadLocal。
2.ThreadLocal的作用
ThreadLocal的作用主要是做数据隔离,ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。
ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
说到隔离,我们应该不难联系到事务的隔离,没错,Spring实现事务隔离采用的就是Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别(@Transaction),巧妙地管理多个事务配置之间的切换,挂起和恢复。
Spring框架里面就是用的ThreadLocal来实现这种隔离,主要是在TransactionSynchronizationManager这个类里面:
值得我们注意的是:Spring的事务主要是ThreadLocal和AOP去做实现的
除此之外,我们在使用SimpleDataFormat时也会用到,可能你在使用SimpleDataFormat时只是简单的new了一个SimpleDataFormat对象,但是在我们使用SimpleDataFormat的parse()方法时,其方法内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。
解决这个问题最简单的办法就是让每个线程都new 一个自己的 SimpleDataFormat就好了,但是有个很大的问题就是如果我们有1000个线程难道new1000个SimpleDataFormat?
所以我们这个时候就可以利用线程池加上ThreadLocal包装SimpleDataFormat,再调用initialValue让每个线程有一个SimpleDataFormat的副本,从而解决了线程安全的问题,也提高了性能。
你以为只有这么多地方可以用到threadLocal???别着急,如果项目中存在一个线程经常遇到横跨若干方法调用,需要传递的对象,也就是上下文(Context),它是一种状态,经常就是是用户身份、任务信息等,就会存在过渡传参的问题。
如果我们使用类似责任链模式,给每个方法增加一个context参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,对象参数就传不进去了,所以我们使用ThreadLocal稍微去做了一下改造,这样只需要在调用前在ThreadLocal中设置参数,其他地方get一下就好了,就像下面那样:
同时,像我们经常使用的cookie,session等数据隔离都是通过ThreadLocal去做实现的。
上面我也提到了ThreadLocal主要是用来做数据隔离使用的,那么它和Synchronized有什么区别呢?
3.ThreadLocal与Synchronized的区别
ThreadLocal其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问。
但是ThreadLocal与synchronized有本质的区别:
1、Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
2、Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。
而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
简单来说ThreadLocal,threadlocl是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry(threadlocl,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。
4.ThreadLocal的底层实现
ThreadLocal<String> localName = new ThreadLocal();
localName.set("张三");
String name = localName.get();
localName.remove();
上面的代码很简单:在ThreadLocal存放一个元素,然后再去获取它最后再把这个元素给移除,整体来说ThreadLocal也就这个三个基本操作:
set、get、remove
我们依次进源码来看一下:
4.1 set

set做的事很简单:主要就是ThreadLocalMap我们需要重点关注一下,而ThreadLocalMap呢是当前线程Thread一个叫threadLocals的变量中获取的。
看到这儿,我们其实就已经发掘出ThreadLocal数据隔离的真相了。
每个线程Thread都维护了自己的threadLocals变量,所以在每个线程创建ThreadLocal的时候,实际上数据是存在自己线程Thread的threadLocals变量里面的,别人没办法拿到,从而实现了隔离。
上面我提到了一个ThreadLocalMap,ThreadLocalMap底层结构是怎么样子的呢?
4.2 ThreadLocalMap

我们先看看上图所示的源码,
既然有个Map那他的数据结构其实是很像HashMap的,但是看源码可以发现,它并未实现Map接口,而且他的Entry是继承WeakReference(弱引用)的,也没有看到HashMap中的next,所以不存在链表了。
我简单说明一下弱引用:弱引用主要应用在不阻止它的key或者value 被回收的mapping,什么意思呢?弱引用的出现就是为了垃圾回收服务的。它引用一个对象,但是并不阻止该对象被回收。如果使用一个强引用的话,只要该引用存在,那么被引用的对象是不能被回收的。弱引用则没有这个问题。在垃圾回收器运行的时候,如果一个对象的所有引用都是弱引用的话,该对象会被回收。

此时就会产生一个问题,没有了链表怎么解决Hash冲突呢?
threadlocalmap 结构就是 entry 数组,我们开发过程中可以一个线程可以有多个TreadLocal来存放不同类型的对象的,但是他们都将放到你当前线程的ThreadLocalMap里,所以肯定要数组来存。
至于具体是如何解决hash冲突的,我们先过一下源码:
从源码里面看到ThreadLocalMap在存储的时候会给每一个ThreadLocal对象一个threadLocalHashCode,在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,int i = key.threadLocalHashCode & (len-1)。
很明显这是很简单的线性探测法,所以解决hash冲突的方式为线性探测法
然后会判断一下:如果当前位置是空的,就初始化一个Entry对象放在位置i上。
如果位置i不为空,如果这个Entry对象的key正好是即将设置的key,那么就刷新Entry中的value;
如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止。
整体流程如下图所示:
由此,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置,set和get如果冲突严重的话,效率还是很低的。
4.3 get

上图便是get的所有过程
说到这里很多人可能在想ThreadLocal的实例以及其值存放在哪里呢?
在Java中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存,而堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问。
但是并不能说ThreadLocal的实例以及其值存放在栈上,虽然threadLocal中值为每个线程所私有,ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有),而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。
5.共享线程的ThreadLocal数据
使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。
public void test() {
final ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("ninesun");
Thread t = new Thread() {
@Override
public void run() {
super.run();
System.out.println("获取存放的值:" + threadLocal.get());
}
};
t.start();
}
好了,我们现在知道了使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,但是这些值是怎么在子线程之间进行传递的呢?
传递的逻辑很简单,
上图是我截取的Thead里面代码片段,thread在初始化创建的时候(即构造函数里)有以下操作:
这段代码也很简单,大致意思是:如果线程的inheritThreadLocals变量不为空,而且父线程的inheritThreadLocals也存在,那么我就把父线程的inheritThreadLocals给当前线程的inheritThreadLocals。比如我们上面的例子。
ThreadLocal已经讲了大半了,可是你可能还没意识到问题的严重性,因为上面提到,key是弱引用,而value却是强引用,如果我们在使用threadLocal操作不当时,就会导致一个很严重的后果:内存泄漏
6.内存泄漏

我们可以看到,ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了。
上图便是key被GC以后的场景。
产生上面这种场景的原因来自于弱引用对象的生命周期
只具有弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
这样说可能不是很直观,我举个简单的例子:
就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。
我们怎么取解决呢?
解决办法太简单了,在代码的最后使用remove就好了,我们只要记得在使用的最后用remove把值清空就好了。
比如,我们之前的代码是:
public void test() {
final ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("ninesun");
}
那么我们就可以通过:
public void test() {
final ThreadLocal threadLocal = new InheritableThreadLocal();
try {
threadLocal.set("ninesun");
} finally {
threadLocal.remove();
}
}

remove的源码也很简单,如上图所示,就是找到对应的值全部置空,这样在垃圾回收器回收的时候,会自动把他们回收掉。
那么问题来了,为啥非得把key设计为弱引用?
7.ThreadLocalMap的key要设计成弱引用?
key不设置成弱引用的话就会造成和entry中value一样内存泄漏的场景。
如果 threadlocalmap 的 key 是强引用, 那么只要线程存在, threadlocalmap 就存在, 而 threadlocalmap 结构就是 entry 数组. 即对应的 entry 数组就存在, 而 entry 数组元素的 key 是 threadLocal。
即便我们在代码中显式赋值 threadlocal 为 null, 告诉 gc 要垃圾回收该对象. 由于上面的强引用存在, threadlocal 即便赋值为 null, 只要线程存在, threadlocal 并不会被回收。
而设置为弱引用, gc扫描到时, 发现threadlocal 没有强引用, 会回收该threadlocal对象。
并且 threadlocal 的 set get remove 都会判断是否 key 为 null, 如果为 null, 那么 value 的也会移除, 之后会被 gc 回收。
ThreadLocal的不足,如解决冲突使用效率最低的线性探测法之类的,可以看看netty的fastThreadLocal来弥补。《谈谈FastLocal为啥这么快》
边栏推荐
- On the operation mechanism of numpy array
- SystemVerilog simulation speed increase
- Summary of config mechanism and methods in UVM (2)
- flutter系列之:flutter中的flow
- UVM中uvm_config_db非直线的设置与获取
- UVM中uvm_report_enabled的用法
- 二叉樹oj題目
- How to convert an old keyboard into a USB keyboard and program it yourself?
- 持续交付-Blue Ocean 应用
- Systematic analysis of social networks using Networkx: Facebook network analysis case
猜你喜欢
![Custom jsp[if, foreach, data, select] tag](/img/a2/fc75c182d572d86f4466323e31d6c3.png)
Custom jsp[if, foreach, data, select] tag

IIS deploy static web site and FTP service

buuctf-pwn write-ups (6)

Clip: learning transferable visual models from natural language monitoring

在连接数据库的时候遇到了点问题,请问怎么解决呀?

Bs-gx-016 implementation of textbook management system based on SSM

JVM 的指针压缩

Summary of working at home during the epidemic | community essay solicitation

XSS笔记(下)

Summary of config mechanism and methods in UVM (2)
随机推荐
Bootstrapblazor + FreeSQL actual combat chart usage (2)
SystemVerilog simulation speed increase
buuctf-pwn write-ups (6)
How to convert an old keyboard into a USB keyboard and program it yourself?
BS-GX-016基于SSM实现教材管理系统
XSS attack notes (Part 1)
[the path of system analyst] Chapter 6: duplicate demand engineering (case paper)
在线文本数字识别列表求和工具
SystemVerilog仿真速率提升
Why divide the training set and the test set before normalization?
leetcode 1143. Longest Commom Subsequence 最长公共子序列(中等)
30《MySQL 教程》MySQL 存储引擎概述
Cookie, sessionstorage, localstorage differences
Memcached foundation 1
Config in UVM_ How to use the DB mechanism
leetcode 1143. Longest common subsequence (medium)
get_ Usage Summary of sequencer
uvm中的config机制方法总结(一)
Bs-gx-016 implementation of textbook management system based on SSM
Kept to implement redis autofailover (redisha) 14
