好久不见啊,今天1024——程序员节嘛;本来今天是不打算创作一篇文章的,因为上周末去了杭州玩了两天,快要累死了,然后今天周一公司事情也可多,想躺平躺平;但是转念一想今天可是程序员节啊,我不表示表示哪多不好意思啊,晚上下班到家吃过饭后,就赶紧打开笔记本来干了。
上周五的时候研究了ThreadLocal源码,自我感觉研究的还算比较透彻,那今天趁着1024正好给大家分享这方面的知识点吧。由于本人知识水平有限,在下面文章中那些不对的地方,恳请大家指出。大家一起进步;好了,废话少说,一起进步。
1、ThreadLocal介绍
1.1、官方介绍
ThreadLocal来提供线程内部局部变量。这种变量能够在多线程环境下,能够保证各个线程之间的线程变量互不干扰。ThreadLocal实例通常来说是private static类型的,用于关联线程和线程上下文切换。
我们能够得知ThreadLocal的作用:提供线程内部的局部变量,不同线程之间不会干扰,这种变量在线程生命周期内起作用,减少同一个线程多个函数和组件的公共变量传递复杂度。
总结:
- 传递变量: 在同一线程内,可以将公共变量存放进ThreadLocal中,然后可以在该线程中任意传递
- 线程隔离: 每个线程的变量都是独立的,不会相互影响
1.2、基本使用
首先来看看几个常用的方法
方法声明 | 描述 |
---|---|
new ThreadLocal() | 创建ThreadLocal对象 |
public void set(T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
使用Demo:
public static void main(String[] args) {
//创建ThreadLocal对象
ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
//设置stringThreadLocal的值value
stringThreadLocal.set("test ThreadLocal");
//获取stringThreadLocal的值
stringThreadLocal.get();
//移除stringThreadLocal的值
stringThreadLocal.remove();
}
上面的例子比较简单,就是简单使用。下面我们来分析内部结构;
2、ThreadLocal内部结构
2.1、常见误解
在刚开始学习之前,我就进入一个大家也会常进的误解就是,认为每个ThreadLocal创建一个Map,然后key是Thread,map的value是我们要存的值,这样就达到了线程之间局部变量隔离效果,早期的ThreadLocal确实是这样设计的,但是在1.8之后就发生了改变,因为这样设计会有弊端——一个ThreadLocal管理一个map,然后key是Thread,当Thread较多时,产生Hash冲突的概率就会比较大,并且维护比较麻烦
。
2.2、现在设计
在JDK1.8之后发生了改变,改变成了一个Thread管理一个map,map的key是ThreadLocal,value是ThreadLocal的值。
具体过程是这样的:
- 每个Thread都负责管理一个Map
- Map的key值是ThreadLocal,value是真正要存储的Object
- Thread内部是靠ThreadLocal来维护,由ThreadLocal来负责set值和get值
- 针对不同的Thread,访问的是不同的Map,那么线程的ThreadLocal就做到了线程隔离,互不干扰
2.3、这样设计好处
这样设计主要有两个以下优势:
- 这样设计之后,那么对应的map值就会减少,发生hash冲突概率会减少;之前存储的数量由Thread决定,现在由ThreadLocal的数量决定;在现实环境中ThreadLocal要比Thread少。
- 当Thread销毁之后,那么对应的ThhreadLocal也会被回收,减少内存使用。
3、ThreadLocal核心源码
3.1、set()方法
//ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals = null;
/**
* set()方法
*/
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程所管理的ThreadLocalMap
ThreadLocalMap map = getMap(t);
//如果已经有了ThreadLocalMap
if (map != null) {
//设置ThreadLocalMap的值,key为ThreadLocal;value就是我们所要设置的值
map.set(this, value);
} else {
//没有ThreadLocalMap则去创建
createMap(t, value);
}
}
/**
* 获取ThreadLocalMap的方法
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
private void set(ThreadLocal<?> key, Object value) {
//获取Thread中的ThreadLocalMap的数组
Entry[] tab = table;
int len = tab.length;
//计算出该key应该存放数组中的位置——key的HashCode对(length-1)与运算
int i = key.threadLocalHashCode & (len-1);
//利用线性探测法来解决hash冲突——当应该存放位置有值时,那么就存放该位置的下一个位置;
//如果下一个位置还有值时,那么存放下下一个,以此类推,当最后一个还不满足,
//那么就会去第0个寻找,可以理解为它就是一个循环数组
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();
}
/**
* 创建ThreadLocalmap
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
* ThreadLocalmap构造方法
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//创建一个length = 16的数组
//长度和hashMap一样,必须为2的n次方
table = new Entry[INITIAL_CAPACITY];
//将key求出的HashCode对(length-1)与运算,(长度为2的n次方的原因在这里就体现出来了)
//得到的值即为该ThreadLocal要存放该数组的位置
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//将数组中该位置的值设置为该value
table[i] = new Entry(firstKey, firstValue);
//数组中存在值的长度
size = 1;
//可以理解为阈值;阈值等于leng*2/3,可以理解为和HashMap的加载因子一样,和它的加载机制也一样
//ThreadLocalMap当存满 > 2/3*length时,会扩容为2倍
setThreshold(INITIAL_CAPACITY);
}
/**
* 求出ThreadLocal的阈值
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
执行流程:
- 求出该Thread
- 取出该Thread管理的ThreadLocalMap
- 如果ThreadLocalMap不为空,则将ThreadLocal作为key,传的Value作为值;
- 如果为空,则去初始化个ThreadLocalMap;并将key,value存放进去
注意:源码的注释中写的比较详细,建议大家好好看看会理解比较透彻
3.2、get()方法
//get方法源码
public T get() {
//求出该线程
Thread t = Thread.currentThread();
//获取该Thread管理的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//Map不为空则获取出该ThreadLocal对应的数组元素
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
//如果元素不为null,则取出该元素对应的value值
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//当map为空或者元素e为空时,会初始化
return setInitialValue();
}
/**
1. 初始化
*/
private T setInitialValue() {
//调用initialValue获取初始化的值
T value = initialValue();
//获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
//在则调用map.set设置此实体entry
map.set(this, value);
} else {
// 1)当前线程Thread 不存在ThreadLocalMap对象
// 2)则调用createMap进行ThreadLocalMap对象的初始化
// 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
// 返回设置的值value
return value;
}
执行流程:
- 首先获取当前线程, 根据当前线程获取一个Map
- 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entry e,否则转到4
- 如果e不为null,则返回e.value,否则转到4
- Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map
总结:先获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始值。
今天先写到这吧,明天还要搬砖,不敢使劲熬夜了。明天早上见。
2022年10月24日23:51:02
3.3、remove()方法
public void remove() {
//获取该Thread管理的ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
//map不为Null
if (m != null) {
//删除该ThreadLocal对应的Entry
m.remove(this);
}
}
执行流程:
- 查找到Thread对应的ThreadLocalMap
- 删除对应ThreadLocal
4、ThreadLocalMap核心源码
通过上面我们了解到线程局部变量存储数据的不是ThreadLocal,而是ThreadLocalMap,key是对应的ThreadLocal,value是传入的Object;一个Thread对应一个ThreadLocalMap;下面我们就来讲讲ThreadLocalMap的核心源码。
4.1、ThreadLocalMap局部变量
/**
* The initial capacity -- MUST be a power of two.
*/
//ThreadLocalMap默认大小,这里基本和HashMap差不多,它的长度同样必须为2的n次方
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
//存放数据的table
private Entry[] table;
/**
* The number of entries in the table.
*/
//数组里面存放个数,可以通过size是否大于阈值来决定ThreadLocalMap是否扩容
private int size = 0;
/**
* The next size value at which to resize.
*/
//进行扩容阈值
private int threshold; // Default to 0
这里和HashMap很相似;有初始容量INITIAL_CAPACITY(必须为2的n次方);table是一个数组Entry[],用来存放数据;size是数组中实际存放的数据元素;threshold是阈值,通过判断size是否大于阈值决定是否扩容。
4.2、存储结构-Entry
/*
* Entry继承WeakReference,并且用ThreadLocal作为key.
* 如果key为null(entry.get() == null),意味着key不再被引用,
* 因此这时候entry也可以从table中清除。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
在ThreadLocalMap中,存储数据用的entry[],entry数据结构是k,v结构,并且key必须是ThreadLocal类型
另外,entry继承了WeakReference,也就是key实现了弱引用,其目的是保证了ThreadLocalMap生命周期和Thread生命周期一样
4.3、弱引用和内存结构
- 强引用: 就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾回收器就不会回收这种对象。
- 弱引用: 垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
ThreadLocal中内部引用(实线表示强引用,虚线表示弱引用)结构图如下所示:
假设在业务代码中使用完ThreaLocal ,Thread Local Ref被回收;
由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例,所以threadlocal就可以顺利被gc回收,此时Entry中的key=null。
但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry -> value ,value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏。
出现内存泄漏真实原因:
- Entry没有手动删除
- Thread没有运行结束
解决方法:
- 针对第一种情况,可以在使用完ThreadLocal后,采用手动remove()即可,就能避免内存泄漏。
- 第二种情况比较难控制了,因为Thread生命周期和ThreadLocal相同生命周期。只有当Thread执行结束,ThreadLocalMap才能被垃圾回收掉。
内存泄漏根源: ThreadLocalMap生命周期和Thread生命周期一样长,没有手动释放删除Entry,造成内存泄漏。
4.4、为什么使用弱引用
当使用弱引用时,ThreadLocal被回收时,ThreadLocalMap的key会被置为null;并且ThreadLocalMap中set和get方法时,会判断key是否为null,如果key为null,会将Entry中置为null。
这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。
4.5、如何解决hash冲突问题
这里采用了线性探测的方法
主要步骤:
-
先利用key的hashCode对(length-1)与运算
-
计算出来的值即为要存放的位置,如果该位置有值了,判断该位置的key值是否与传来的key相等,如果相等,则直接将value替换成新的;不相等的话,则查找下一个位置,如果下一个位置为null,插入;不为null,如果key相等,则替换;如果key不相等,则继续查找下一个;循环往复这样,如果最后一个都没找到,那么会查找entry[0],你可以理解他为一个循环数组。
-
如果该位置为null,则直接插入
最后调用cleanSomeSlots,清理key为null的Entry,最后返回是否清理了Entry,接下来再判断sz 是否>= thresgold达到了rehash的条件,达到的话就会调用rehash函数执行一次全表的扫描清理。5、ThreadLocal和Synchroized对比
-
Synchronized: 它采用的是加锁同步的方式,只提供一个变量,保证只有线程排队执行;采用的是时间换取空间的思想;这样效率比较慢。
-
ThreadLocal: 采用的是空间换取时间方式,每个线程都存放一份局部变量,从而实现不同的访问互不影响。
总结: Synchronized是多线程间同步,ThreadLocal是多线程间相互隔离,ThreadLocal并发高。
好了,这篇文章就到这吧,由于时间比较紧,也没有好好打磨,有写的不对还请多多指出。
大家一起分享,一起加油,奥利给。大家一键三连哦。。。。