文章目录
1 ThreadLocal
1.1 基本理解
ThreadLocal
是 JDK
底层提供的一个解决多线程并发问题的工具类,它为每个线程提供了一个本地的副本变量机制
,实现了和其它线程隔离,并且这种变量只在本线程的生命周期
内起作用,可以减少同一个线程内多个方法之间的公共变量传递的复杂度,从而起到线程隔离的作用,避免了线程安全问题
1.2 应用
1.2.1 使用场景
ThreadLocal
并不是为了解决多线程共享变量的问题,比如商品的库存数量这种场景下是不能使用ThreadLocal
的。ThreadLocal
是多线程都需要使用一个变量,但是这个变量的值不需要各个线程间共享,每个线程都有自己的这个变量的值。
ThreadLocal
还有一种场景是 在 API
层,我们经常需要 request
这个参数,我们可能就需要在很多场景下使用这个参数,但是每个方法都把它作为参数的话会让方法的参数过多不好维护,所以我们可以把这些 request
都对应到一个线程上面,一个线程内如果想使用这个参数,直接去取就行了。
简而言之就是每个线程拥有自己的实例
,然后实例需要在对应线程的使用的多个方法中共享但是不希望被多线程共享。
主要解决2类问题:
并发问题
:使用ThreadLocal
代替Synchronized
来保证线程安全,同步机制采用空间换时间 -> 仅仅先提供一份变量,各个线程轮流访问,后者每个线程都持有一份变量,访问时互不影响。数据存储问题
:ThreadLocal
为变量在每个线程中创建了一个副本,所以每个线程可以访问自己内部的副本变量。
1.2.2 在 Spring 中的使用->解决线程安全问题
一般情况下,只有无状态的 Bean
才会在各个实例中共享,在 Spring
中绝大多数的 Bean
都可以声明为 singleton
单例的
比如一些 request
相关的非线程安全状态采用了ThreadLocal
让它们成为线程安全的状态
一般情况下,web
应用划分成 MVC
三层,在不同的层次中编写对应的逻辑,下层通过接口向上层开放功能调用,正常情况下,从接收请求到响应都应该属于同一个线程。而 ThreadLocal
是一个很好的机制,它为每个线程提供了一个独立的变量副本解决了变量并发访问的冲突问题,比 Synchronized
要简单且方便,可以让程序具备更高的并发性
1.2.3 SimpleDateFormat线程安全问题
SimpleDateFormat
线程是不安全的
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dateFormat();
}
});
thread.start();
}
}
/**
* 字符串转成日期类型
*/
public static void dateFormat() {
try {
simpleDateFormat.parse("2021-5-27");
} catch (ParseException e) {
e.printStackTrace();
}
}
这里我们只启动了50个线程问题就会出现,其实看巧不巧,有时候只有10个线程的情况就会出错:
Exception in thread "Thread-40" java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at cn.haoxy.use.lock.sdf.SimpleDateFormatTest.dateFormat(SimpleDateFormatTest.java:36)
at cn.haoxy.use.lock.sdf.SimpleDateFormatTest$1.run(SimpleDateFormatTest.java:23)
at java.lang.Thread.run(Thread.java:748)
我们可以借助线程池加上ThreadLocal
来解决这个问题
public class SimpleDateFormatTest {
private static ThreadLocal<SimpleDateFormat> local = new ThreadLocal<SimpleDateFormat>() {
@Override
//初始化线程本地变量
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
public static void main(String[] args) {
ExecutorService es = Executors.newCachedThreadPool();
for (int i = 0; i < 500; i++) {
es.execute(() -> {
//调用字符串转成日期方法
dateFormat();
});
}
es.shutdown();
}
/**
* 字符串转成日期类型
*/
public static void dateFormat() {
try {
//ThreadLocal中的get()方法
local.get().parse("2021-5-27");
} catch (ParseException e) {
e.printStackTrace();
}
}
}
1.3 原理
1.3.1 ThreadLocal的结构
ThreadLocal
主要分为2个部分:
- 第一部分是它的一些成员属性,这部分主要和计算哈希值相关的。
- 另一部分是它对外提供的几个
API
,这些方法可以操作它自己内部非常重要的内部类ThreadLocalMap
所以说它才是ThreadLocal
的底层实现
不同版本 ThreeadLocal
原理:
- 早期版本:
ThreadLocal
是每个ThreadLocal
类都会去创建一个Map
,然后以线程id
作为key
,要存储的局部变量作为value
,这样就可以达到线程隔离的效果。但是这样的话,这个存储数量是Thread
的数量决定,当线程销毁之后还要去维护Map
中的那份k-v
让它也随之销毁。 - 后来版本:每个线程都维护一个
ThreadlocalMap
哈希表(类似HashMap
),这个哈希表的key
是ThreadLocal
对象本身,value
是要存储的局部副本值
,这样的话存储数量是ThreadLocal
的数量决定的。当Thread
销毁之后,ThreadLocalMap
也会被随之销毁,减少内存占用。
ThreadLocal内存结构图
ThreadLocalMap
原理:
ThreadLocalMap
的实现原理跟 HashMap
差不多,内部有一个 Entry
数组,一个 Entry
通常至少包括key,value
,特殊的是这个Entry
继承了 WeakReference
也就是说它是弱引用的所以可能会有 内存泄露
的情况。这个后面再说。ThreadLocal
负责管理 ThreadLocalMap
,包括插入,删除 等等
另一方面来说 ThreadLocal
基本上就相当于门面设计模式中的一个Facade
类。key
就是 ThreadLocal
对象自己,同时,很重要的一点:就ThreadLocal
把 Map
存储在当前线程对象里面
Thread
对象中持有一个ThreadLocal.ThreadLocalMap
的成员变量。ThreadLocalMap
内部维护了Entry
数组,每个Entry
代表一个完整的对象,key
是ThreadLocal
本身,value
是ThreadLocal
的泛型值。每个线程在往ThreadLocal
里设置值的时候,都是往自己的ThreadLocalMap
里存,读也是以某个ThreadLocal
作为引用,在自己的map
里找对应的key
,从而实现了线程隔离
对于ThreadLocal
有2
点需要注意:
ThreadLocal
实例本身是不存储值,它只是提供了一个在当前线程中找到副本值的key
(它自己就是ThreadLocalMap
的key
)ThreadLocal
包含在Thread
中,而不是Thread
包含在ThreadLocal
中
1.3.2 ThreadLocal的内部类ThreadLocalMap
ThreadLocalMap 的源码稍微多了点,我们就看两个最核心的方法
getEntry(ThreadLocal<?> key)
set(ThreadLocal> key, Object value)
这个set()
操作和我们在集合了解的put()
方式有点不一样,虽然他们都是key-value
结构,不同在于他们解决散列冲突的方式不同。
集合Map
的put()
采用的是拉链法,而ThreadLocalMap
的set()
则是采用开放定址法,开放地址法就是不会有链式的结构,如果冲突了,以当前位置为基准再找一个判断,直到找到一个空的地址。
点击了解更多hashmap冲突的解决方法
set()
操作除了存储元素外,还有一个很重要的作用,就是replaceStaleEntry()
和cleanSomeSlots()
,这两个方法可以清除掉key == null
的实例,防止内存泄漏。
get()
方法有一个重要的地方当key == null
时,调用了expungeStaleEntry()
方法,该方法用于处理key == null
,有利于GC
回收,能够有效地避免内存泄漏
1.3.2.1 ThreadLocalMap和HashMap区别
ThreadLocalMap
和HashMap
的功能类似,但是实现上却有很大的不同:
HashMap
的数据结构是数组+链表ThreadLocalMap
的数据结构仅仅是数组HashMap
是通过链地址法(点击了解更多hashmap冲突的解决方法)解决hash
冲突的问题ThreadLocalMap
是通过开放地址法来解决hash
冲突的问题HashMap
里面的Entry
内部类的引用都是强引用ThreadLocalMap
里面的Entry
内部类中的key
是弱引用,value
是强引用
1.3.2.2 链地址法和开放地址法区别
jdk
中大多数的类都是采用了链地址法来解决hash 冲突,为什么ThreadLocalMap
采用开放地址法来解决哈希冲突呢?首先我们来看看这两种不同的方式
链地址法
这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i
个单元中,因而查找、插入和删除主要在同一链中进行。列如对于关键字集合{12,67,56,16,25,37, 22,29,15,47,48,34},我们用12为除数,进行除留余数法:
开放地址法
这种方法的基本思想是一旦发生了冲突,就去寻找下一个空的散列地址(这非常重要,源码都是根据这个特性,必须理解这里才能往下走),只要散列表足够大,空的散列地址总能找到,并将记录存入。
比如说,我们的关键字集合为{12,33,4,5,15,25},表长为10。 我们用散列函数f(key) = key mod 10
。 当计算前S个数{12,33,4,5}时,都是没有冲突的散列地址,直接存入(蓝色代表为空的,可以存放数据):
计算key = 15时,发现f(15) = 5
,此时就与5所在的位置冲突。于是我们应用上面的公式f(15) = (f(15)+1) mod 10 =6
。于是将15存入下标为6的位置。这其实就是房子被人买了于是买下一间的作法:
链地址法和开放地址法的优缺点- 开放地址法:
容易产生堆积问题,不适于大规模的数据存储。
散列函数的设计对冲突会有很大的影响,插入时可能会出现多次冲突的现象。
删除的元素是多个冲突元素中的一个,需要对后面的元素作处理,实现较复杂。 - 链地址法:
处理冲突简单,且无堆积现象,平均查找长度短。
链表中的结点是动态申请的,适合构造表不能确定长度的情况。
删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。
指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间。
1.3.2.3 ThreadLocalMap 采用开放地址法原因
ThreadLocal
中有一个属性HASH_INCREMENT = 0x61c88647
,0x61c88647
是一个神奇的数字,让哈希码能均匀的分布在2的N次方
的数组里, 即Entry table
,关于这个神奇的数字google 有很多解析,这里就不重复说了ThreadLocal
往往存放的数据量不会特别大(而且key
是弱引用又会被垃圾回收,及时让数据量更小),这个时候开放地址法简单的结构会显得更省空间,同时数组的查询效率也是非常高,加上第一点的保障,冲突概率也低
1.3.3 ThreadLocal是如何定位数据的
由于ThreadLocalMap
对象底层是用Entry
数组保存数据的。
那么问题来了,ThreadLocal
是如何定位Entry
数组数据的?
在ThreadLocal
的get、set、remove
方法中都有这样一行代码:
int i = key.threadLocalHashCode & (len-1);
通过key
的hashCode
值,与数组的长度减1。其中key
就是ThreadLocal
对象,与数组的长度减1,相当于除以数组的长度减1,然后取模。
这是一种hash算法。
接下来给大家举个例子:
假设len=16,key.threadLocalHashCode=31,
于是: int i = 31 & 15 = 15
相当于:int i = 31 % 16 = 15
计算的结果是一样的,但是使用与运算效率跟高一些。
为什么与运算效率更高?
因为
ThreadLocal
的初始大小是16,每次都是按2倍扩容,数组的大小其实一直都是2的n次方
。这种数据有个规律就是高位是0,低位都是1。在做与运算时,可以不用考虑高位,因为与运算的结果必定是0。只需考虑低位的与运算,所以效率更高。
如果使用hash
算法定位具体位置的话,就可能会出现hash
冲突的情况,即两个不同的hashCode
取模后的值相同。
ThreadLocal
是如何解决hash
冲突的呢?
我们看看getEntry
是怎么做的:
private Entry getEntry(ThreadLocal<?> key) {
//通过hash算法获取下标值
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//如果下标位置上的key正好是我们所需要寻找的key
if (e != null && e.get() == key)
//说明找到数据了,直接返回
return e;
else
//说明出现hash冲突了,继续往后找
return getEntryAfterMiss(key, i, e);
}
再看看getEntryAfterMiss方法:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
//判断Entry对象如果不为空,则一直循环
while (e != null) {
ThreadLocal<?> k = e.get();
//如果当前Entry的key正好是我们所需要寻找的key
if (k == key)
//说明这次真的找到数据了
return e;
if (k == null)
//如果key为空,则清理脏数据
expungeStaleEntry(i);
else
//如果还是没找到数据,则继续往后找
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
关键看看nextIndex
方法:
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
当通过hash算法计算出的下标小于数组大小,则将下标值加1。否则,即下标大于等于数组大小,下标变成0了。下标变成0之后,则循环一次,下标又变成1。。。
寻找的大致过程如下图所示:
如果找到最后一个,还是没有找到,则再从头开始找。
不知道你有没有发现,它构成了一个:环形。
ThreadLocal
从数组中找数据的过程大致是这样的:
- 通过
key
的hashCode
取余计算出一个下标。 - 通过下标,在数组中定位具体
Entry
,如果key正好是我们所需要的key,说明找到了,则直接返回数据。 - 如果第2步没有找到我们想要的数据,则从数组的下标位置,继续往后面找。
- 如果第3步中找key的正好是我们所需要的key,说明找到了,则直接返回数据。
- 如果还是没有找到数据,再继续往后面找。如果找到最后一个位置,还是没有找到数据,则再从头,即下标为0的位置,继续从前往后找数据。
- 直到找到第一个Entry为空为止。
1.3.4 ThreadLocal是如何扩容的
从上面得知,ThreadLocal
的初始大小是16。那么问题来了,ThreadLocal
是如何扩容的?
在set
方法中会调用rehash
方法:
private void set(ThreadLocal<?> key, Object value) {
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)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
注意一下,其中有个判断条件是:sz(之前的size+1)
如果大于或等于threshold
的话,则调用rehash
方法。
threshold
默认是0,在创建ThreadLocalMap
时,调用它的构造方法:
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
调用setThreshold
方法给threshold
设置一个值,而这个值INITIAL_CAPACITY
是默认的大小16
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
也就是第一次设置的threshold = 16 * 2 / 3
, 取整后的值是:10
换句话说当sz大于等于10时,就可以考虑扩容了。
rehash代码如下:
private void rehash() {
//先尝试回收一次key为null的值,腾出一些空间
expungeStaleEntries();
if (size >= threshold - threshold / 4)
resize();
}
在真正扩容之前,先尝试回收一次key为null的值,腾出一些空间。
如果回收之后的size大于等于threshold的3/4时,才需要真正的扩容。
计算公式如下:
16 * 2 * 4 / 3 * 4 - 16 * 2 / 3 * 4 = 8
也就是说添加数据后,新的size大于等于老size的1/2时,才需要扩容。
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
//按2倍的大小扩容
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
resize中每次都是按2倍的大小扩容。
扩容的过程如下图所示:
扩容的关键步骤如下:
- 老size + 1 = 新size
- 如果新size大于等于老size的2/3时,需要考虑扩容。
- 扩容前先尝试回收一次key为null的值,腾出一些空间。
- 如果回收之后发现size还是大于等于老size的1/2时,才需要真正的扩容。
- 每次都是按2倍的大小扩容。
1.4 各类问题总结
1.4.1 问题1:弱引用相关问题
ThreadLocalMap
中存储实体Entry
使用当前 threadLocal
实例作为key
,但是这个Entry
继承了弱引用WeakReference
,什么是弱引用?为什么要这么设计?这样会带来什么问题?
先说下什么是强引用,什么是弱引用:
只要垃圾回收机制一运行,不管JVM
的内存空间是否充足,都会回收该对象占用的内存
点击了解更多java中四种引用(强,软,弱,虚)类型区别
回到 ThreadLocalMap
的层面来看为啥哈希表的节点要实现WeakReference
弱引用。也就是ThreadLocalMap
中的key
使用Threadlocal
实例作为弱引用。如果一个ThreadLocal
没有外部引用去引用它,那么在系统GC
的时候它势必要被回收的。这样一来ThreadLocalMap
中就会出现key
为null
的entry
就没有办法访问这些key
为null
的Entry
的value
。如果线程一直不能结束的话,就会存在一条强引用链:ThreadLocalRef->Thread->ThreadLocal->ThreadLocalMap->Entry->value
永远无法被回收造成内存泄露。其实在ThreadLocalMap
的设计中为了防止这种情况,也有一些防护措施,比如新增
、移除
、获取
的时候都会去擦除key==null
的value
。
但是这些措施并不能保证一定不会内存泄露,比如:
- 使用了
static
修饰的ThreadLocal
,延长了ThreadLocal
的生命周期,可能会导致内存泄露
。 - 分配使用了
ThreadLocal
又不再调用get ,set ,remove
方法也会导致内存泄露。
从表面上看内存泄漏的根源在于使用了弱引用。网上的文章大多着重分析ThreadLocal
使用了弱引用会导致内存泄漏,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?
官方给的说法是: 为了应对非常大和长时间的用途,哈希表使用弱引用的 key
。我们假设我们自己设计的时候key
使用的强引用和弱引用
key
使用强引用:如果引用ThreadLocal
的对象ThreadLocalRef
被回收了,但是ThreadLocalMap
还持有ThreadLocal
对象的强引用,如果没有手动删除的话ThreadLocal
不会被回收,这样会导致Entry
内存泄露key
使用弱引用:引用的ThreadLocal
的对象ThreadLocalRef
被回收了,由于ThreadLocalMap
持有ThreadLocal
的弱引用,即使没有手动删除,ThreadLocal
也会被回收。value
在下一次ThreadLocalMap
调用get(),set(),remove()
的时候会被清除。
比较上面的2种情况,我们会发现:ThreadLocalMap
的生命周期和Thread
一样长,如果都没有手动删除key
都会导致内存泄露。但是弱引用多了一层保障,就是value
在下一次ThreadLocalMap
调用 get(),set(),remove()
的时候会被清除。
因此可知,ThreadLocal
发生内存泄露的根源是由于ThreadLocal
的生命周期和Thread
一样长,在没有手动删除对应的key
的时候就会导致内存泄露,并不是因为弱引用导致的,弱引用只是优化的方式。
综上分析:为了避免内存的泄露,每次使用完ThreadLocal
的时候都需要调用 remove()
方法来擦除数据。并且大规模网站一般都会使用到线程池,如果没有及时清理的话不仅是内存泄露,业务逻辑可能也会被影响。所以养成好习惯,记得擦除数据。
图示分析:
如图所示存在一条引用链:Thread Ref->Thread->ThreadLocalMap->Entry->Key:Value
,经过上面的讲解我们知道ThreadLocal
作为Key
,但是被设置成了弱引用,弱引用在JVM
垃圾回收时是优先回收的,就是说无论内存是否足够弱引用对象都会被回收;弱引用的生命周期比较短;当发生一次GC的时候就会变成如下
TreadLocalMap
中出现了Key
为null
的Entry
,就没有办法访问这些key为null的Entry的value,如果线程迟迟不结束(也就是说这条引用链无意义的一直存在)就会造成value永远无法回收造成内存泄露;如果当前线程运行结束Thread
,ThreadLocalMap
和Entry
之间没有了引用链,在垃圾回收的时候就会被回收;但是在开发中我们都是使用线程池的方式,线程池的复用不会主动结束;所以还是会存在内存泄露问题;解决方法也很简单,就是在使用完之后主动调用remove()
方法释放掉
1.4.2 问题2:ThreadLocal和synchronized区别
说一说ThreadLocal
和synchronized
的区别?
ThreadLocal
和synchronized
都是用来处理多线程环境下并发访问变量的问题,只是二者处理的角度
不同、思路
不同。
ThreadLocal
是一个类,通过对当前线程中的局部变量操作来解决不同线程的变量访问的冲突问题。所以ThreadLocal
提供了线程安全
的共享对象机制,每个线程都拥有其副本。
Java
中的synchronized
是一个保留字,它依靠JVM
的锁机制来实现临界区的函数或者变量的访问中的原子性。在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。此时,被用作锁机制
的变量是多个线程共享的。
同步机制(synchronized
关键字)采用了以时间换空间
的方式,提供一份变量,让不同的线程排队访问。而ThreadLocal
采用了以空间换时间
的方式,为每一个线程都提供一份变量的副本,从而实现同时访问而互不影响。
1.4.3 问题3:原理总结
- 每个
Thread
维护着一个ThreadLocalMap
的引用 ThreadLocalMap
是ThreadLocal
的内部类,用Entry
来进行存储- 调用
ThreadLocal
的set()
方法时,实际上就是往ThreadLocalMap
设置值,key
是ThreadLocal
对象,值是传递进来的对象 - 调用
ThreadLocal
的get()
方法时,实际上就是往ThreadLocalMap
获取值,key
是ThreadLocal
对象 ThreadLocal
本身并不存储值,它只是作为一个key
来让线程从ThreadLocalMap
获取value
正是因为这几点,所以能够实现数据隔离,获取当前线程的局部变量值,和其它线程无关。
1.4.4 使用ThreadLocal时对象存在哪里
在java
中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有变量,而堆内存中的变量对所有线程可见,可以被所有线程访问
那么ThreadLocal
的实例以及它的值是不是存放在栈上呢?其实不是的,因为ThreadLocal
的实例实际上也是被其创建的类持有,(更顶端应该是被线程持有),而ThreadLocal
的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见
1.5 InheritableThreadLocal
1.5.1 线程之间怎么传递ThreadLocal对象
在实际开发中,我们经常会使用 ThreadLocal
传递日志的 requestId
,以此来获取整条的请求链路记录下来方便排查问题。
然而当一个线程中开启了其它的线程,此时的 Threadlocal
里面的数据就会无法获取。比如下面的代码最开始获取到的就是Null
。因为不是同一个线程,所以理所当然输出的值为Null
,如果要实现父子线程
通信,这个问题在Threadlocal
的子类 InheritableThreadLocal
已经有对应的实现了,通过这个实现,可以实现父子线程之间的数据传递,在子线程中能够使用父线程的ThreadLocal
本地变量。InheritableThreadLocal
继承了ThreadLocal
并且重写了三个相关的方法,具体处理大致是之前的ThreadLocal
获取 ThreadlocalMap
的时候一般都是用 this
,在这里都是Thread
先获取父线程,然后将父线程的 ThreadLocalMap
传递给子线程
但是阿里巴巴编码规范插件说了啥?不要显示创建线程
,请使用线程池
!所以下面我们用线程池来试试!
如下代码所示,当线程池的核心线程数设置为1
的时候,2
次输出的结果都是 我是主线程1
。ThreadPoolManage
是本地写的一个线程池实现,github
上有源码。原因相信都能踩到了,线程池会缓存使用过的线程,第一个任务来的时候创建一个线程,此时线程空闲了,第二次来任务还是会使用这个线程,所以就会出现下面的问题了。如何解决?阿里的transmittable-thread-local
提供了解决方案,思路是,InheritableThreadLocal
虽然可以完成父子线程
的传递,但是对于使用了线程池的情况线程是让线程池去创建好的,然后拿来复用的,这个时候父子线程传递 ThreadLocalMap
的引用没有意义了,应用需要的是把任务提交给线程池时候把 ThreadLocalMap
传递到任务去执行。感兴趣在阿里的github
上有,已经开源的。
/**
* ThreadLocalTestExecutor
* @since 2019/1/23 下午11:00
*/
public class ThreadLocalExecutorTest {
private static ThreadPoolManager threadPoolManager = ThreadPoolManager.INSTANCE;
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
threadLocal.set("我是主线程1");
threadPoolManager.addExecuteTask(()->{
System.out.println(threadLocal.get());
return null;
});
threadLocal.set("我是主线程2");
threadPoolManager.addExecuteTask(()->{
System.out.println(threadLocal.get());
return null;
});
//当线程池核心线程数为1的时候2次输出都是 我是主线程1
}
}
1.5.2 InheritableThreadLocal是如何弥补ThreadLocal不支持继承的特性,它的实现原理是啥
ThreadLocal
和InheritableThreadLocal
本质上只是为了方便编码给的工具类,具体存数据是ThreadLocalMap
对象。
ThreadLocalMap
存的key
对象是ThreadLocal
,value
就是真正需要存的业务对象。
Thread
里通过两个变量持用ThreadLocalMap
对象,分别为:threadLocals
和inheritableThreadLocals
InheritableThreadLocal
之所以能够完成线程间变量的传递,是在new Thread()
的时候对inheritableThreadLocals
对像里的值进行了复制。
子线程通过继承得到的InheritableThreadLocal
里的值与父线程里的InheritableThreadLocal
的值具有相同的引用,如果父子线程想实现不影响各自的对象,可以重写InheritableThreadLocal
的childValue
方法
InheritableThreadLocal
在子线程创建的时候把父线程的 ThreadLocalMap
传递给它,它继承 ThreadLocal
并重写了3个方法,并使用 Thread.inheritableThreadLocals
代替了 Thread.threadlocals
字段
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);
}
}
1.6 TransmittableThreadLocal
1.6.1 定义
TransmittableThreadLocal
是阿里开源的一个类,主要目的是处理父子线程变量不能共用的情况。ThreadLocal
是跟当前线程挂钩的,所以脱离当前线程它就起不了作用。
ThreadLocal
目的就是为了保证当每个线程都能有一个独有的变量InheritableThreadLocal
InheritableThreadLocal
和ThreadLocal
的区别是,InheritableThreadLocal
定义的变量是父->子线程可传递的。其实就是在new Thread()
的时候,判断下父线程是否定义了InheritableThreadLocal
,如果有,就拷贝一份ThreadLocalMap
。TransmittableThreadLocal
这个就是就是实现线程池中创建好的线程可以进行值传递,无法传递值的问题。
由于线程池的中的线程只能创建一次,回到InheritableThreadLocal
上面说的步骤,传递的时机只有new Thread()
才会出现。队列未满,线程池到了最大核心线程数就会停止创建,在这些线程未销毁前,父线程更新InheritableThreadLocal
定义的变量,线程池中的线程拿的还是之前的InheritableThreadLocal
变量的值。
那TransmittableThreadLocal
怎么实现线程池中线程能进行值传递呢?
我们可以想到线程池执行原理,或者说线程执行的方法本质就是run()
方法。对Runnable
做了个增强(TtlRunnable
)。在真正的run
方法执行前,会把父线程的本地变量replay
给子线程。run
方法结束后,就恢复为原来状态。
InheritableThreadLocal
解决父子线程的问题,它是在线程创建的时候进行复制上下文的。那么对于线程池的已经创建完了就无从下手了,所以在线程提交的时候要进行上下文的复制。这就是TransmittableThreadLocal
想要解决的问题
1.6.2 pom.xml
TransmittableThreadLocal
是阿里开源的一个类,因此需要阿里的pom.xml
坐标
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.2.0</version>
</dependency>