1、简单概述
在研究多线程的时候,处理多线程共享变量,使用锁来解决之外;但是有时候并非是想只通过这样一种方式来进行解决,而是想要每个线程都使用唯一的一个变量来进行使用,比如说解决数据库的一致性问题,通过同一个connection来进行解决。
那么就是通过ThreadLocal的方式来进行解决,并非是通过线程同步的方式,而是使用线程隔离的方式来进行解决的。
参考博客:https://blog.csdn.net/weixin_44050144/article/details/113061884
从Java官方文档中的描述:ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。
我们可以得知 ThreadLocal 的作用是:提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。
从这段话中提取出来关键的信息:每个线程中的变量是全局变量的一份拷贝,而在每个线程中单独操作时不会影响到其他线程中该变量的值,这样达到的效果就是线程之间相互隔离了。而且从最后一句话中可以得知,可以在同一个线程中的多个函数调用之间达到变量传递的效果。
也就是说我们可以利用这个技巧可以解决实际上的问题,比如说,方法扩展上,如果方法需要多加一个参数,那么我们就可以利用线程隔离的手段,在线程上绑定上一个变量,而不是需要将方法立即给修改掉,从而达到想要的效果。
总结一下ThreadLocal的使用场景介绍:
总结:
1. 线程并发: 在多线程并发的场景下;
2. 传递数据: 我们可以通过ThreadLocal在同一线程不同组件、函数中传递公共变量
3. 线程隔离: 每个线程中的变量都是独立的,不会互相影响。也就是说实现了多线程之间对同一个共享数据的隔离;
但是需要注意区别,在选择使用的时候注意二者的使用场景是有着很大的不同的。一个是在线程同步条件下,而threadlocal不需要保证线程同步,它强调的是线程隔离的条件下来使用变量,所以在这种情况下,不算叫做共享变量。
在使用之前,我们先来认识几个ThreadLocal的常用方法
方法声明 | 描述 |
---|---|
ThreadLocal() | 创建ThreadLocal对象 |
public void set( T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
我们来看下面这个案例 , 感受一下ThreadLocal 线程隔离的特点:
public class MyDemo {
private String content;
private String getContent() {
return content;
}
private void setContent(String content) {
this.content = content;
}
public static void main(String[] args) {
MyDemo demo = new MyDemo();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
demo.setContent(Thread.currentThread().getName() + "的数据");
System.out.println("-----------------------");
System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
}
});
thread.setName("线程" + i);
thread.start();
}
}
}
从结果可以看出多个线程在访问同一个变量的时候出现的异常,线程间的数据没有隔离。下面我们来看下采用 ThreadLocal 的方式来解决这个问题的例子。
因为什么?使用使用的是同一个对象来进行操作的。对象最终展示的是能够及时看到的对象的属性值。
那么使用ThreadLocal来观察:
public class MyDemo1 {
private static ThreadLocal<String> tl = new ThreadLocal<>();
private String content;
private String getContent() {
return tl.get();
}
private void setContent(String content) {
tl.set(content);
}
public static void main(String[] args) {
MyDemo demo = new MyDemo();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
demo.setContent(Thread.currentThread().getName() + "的数据");
System.out.println("-----------------------");
System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
}
});
thread.setName("线程" + i);
thread.start();
}
}
}
将结果打印到控制台,查看输出信息,可以看到线程之间实现了隔离。从这里可以观察出来什么内容?及时是使用对象来进行操作,但是对象操作的方法中是通过threadlocal来进行操作的。
转账的案例先不看了,具体的来看下源代码。
2、ThreadLocal结构
首先看下ThreadLocal的结构:
早期的设计结构图如下所示:
对于一个ThreadLocal来说,里面维护了一个ThreadLocalMap,map集合中维护了一个Entry,key是每个thread,value就是每个线程的绑定的值。
这种方式比较符合人们的理解方式。但是这种方式在之后就给废弃了。但是没有对比就没有伤害,看看最新的方式
jdk8中的设计方式:
可以看到每个线程维护了一个ThreadLocalMap,而每个ThreadLocalMap中存在着一个Entry,其中key是threadlocal,value就是每个线程保存的孩子。
可以看到这里和上面的区别就在于:将threadlocal和thread调换了一个位置。
所以这里简单介绍下区别:
1、对于多线程环境下来说,每个线程对象都有一个threadlocalmap,这个是肯定的,但是每个threadlocal是否都有着对应的值呢?
也就是说是否存在着Entry?也就是说在真实场景下threadlocalmap是存在的,但是并不一定存在着entry。也就是说threadlocalmap的数量是少于thread的,因为有的线程可能并没有使用到threadlocalmap,换句话说为null
2、thread销毁之后,threadlocalmap也会随之消失,因为threadlocalmap是依赖于thread而存在的。既然thread消失了,那么threadlocalmap也就被垃圾回收了
在看过源码之后,会发现里面有这样的一个特点,里面的entry中的key继承了弱引用,那么聊一下弱引用和内存泄漏的问题。
内存泄漏:这个在底层语言c和c++中是很好理解的,也就是说程序员手动的来分配内存,在使用完毕之后,需要手动的将内存释放掉。保存安全性。如果没有进行释放,那么程序的其他地方也就无法来访问这块内存空间。如果出现了大量的这种情况,总会导致程序越来越卡,导致程序最终无法运行。
内存溢出:这种也是比较常见的场景,比如说递归方法中最常用的,也就是调用方法的时候,导致了OS分配给程序的可用空间不足以满足程序的运行了,最终导致了这种现象的发生。
弱引用:因为java程序没有了C语言和c++的手动释放的习惯,而是采用了垃圾回收算法来将程序员在请求操作系统来分配内存后,使用完成之后没有手动释放的情况,采用了垃圾回收算法来将这里JVM认为是“垃圾”的内存空间来进行清楚掉。java中的引用我们可以看成是指针,java中分为了强软弱虚的概念。这里将一下弱引用的概念,其实其他的也是差不多的,弱引用就是只要是JVM检测到了,那么就可以进行回收掉。
假设ThreadLocalMap中的key使用了强引用,那么会出现内存泄漏吗?
此时ThreadLocal的内存图(实线表示强引用)如下:
假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。
但是因为threadLocalMap的Entry中的key强引用了threadLocal,造成threadLocal无法被回收。
在没有手动删除这个Entry以及CurrentThread依然运行的前提下,始终有强引用链 threadRef->currentThread->threadLocalMap->entry,Entry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。
也就是说,ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的。
那么ThreadLocalMap中的key使用了弱引用,会出现内存泄漏吗?
此时ThreadLocal的内存图(实线表示强引用,虚线表示弱引用)如下:
同样假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。
由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收,此时Entry中的key=null。
但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry -> value ,value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏。
也就是说,ThreadLocalMap中的key使用了弱引用, 也有可能内存泄漏。
从上面可以看到,currentThread仍然在运行的情况下,key是强引用的时候,key泄漏;如果key是弱引用的情况下,value存在着内存泄漏。
那么根本原因是什么?首先假想一下,如果currentthread没有了指向的时候,那么也就说明了map没有了指向,此时map也就没有了外部的指向是否是可行的?
线程运行结束之后,每个线程都会被GC给回收掉,map也会回收掉,最终的Entry也会被回收掉。
但是如果说当前线程依然存在的情况下,如何来进行处理?首先线程依然存在的时候,当使用完了局部变量之后,手动的将Entry给移除掉。
对象的属性依赖于当前的对象而存在,所以属性的依赖是依赖于对象的。但是我们应当适量的避免这种依赖性强的,低内聚。
也就是说尽量减少依赖性,但是无法完全避免这种依赖。
3、源码分析
接下来看看源码:
先看下get方法:
/**
* 设置当前线程对应的ThreadLocal的值
*
* @param value 将要保存在当前线程对应的ThreadLocal的值
*/
public void set(T value) {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 判断map是否存在
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);
}
/**
* 获取当前线程Thread对应维护的ThreadLocalMap
*
* @param t the current thread 当前线程
* @return the map 对应维护的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
*创建当前线程Thread对应维护的ThreadLocalMap
*
* @param t 当前线程
* @param firstValue 存放到map中第一个entry的值
*/
void createMap(Thread t, T firstValue) {
//这里的this是调用此方法的threadLocal
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
总结下流程:
A. 首先获取当前线程,并根据当前线程获取一个Map
? B. 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)
? C. 如果Map为空,则给该线程创建 Map,并设置初始值
再看下set方法:
/**
* 返回当前线程中保存ThreadLocal的值
* 如果当前线程没有此ThreadLocal变量,
* 则它会通过调用{@link #initialValue} 方法进行初始化值
*
* @return 返回当前线程对应此ThreadLocal的值
*/
public T get() {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null) {
// 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
ThreadLocalMap.Entry e = map.getEntry(this);
// 对e进行判空
if (e != null) {
@SuppressWarnings("unchecked")
// 获取存储实体 e 对应的 value值
// 即为我们想要的当前线程对应此ThreadLocal的值
T result = (T)e.value;
return result;
}
}
/*
初始化 : 有两种情况有执行当前代码
第一种情况: map不存在,表示此线程没有维护的ThreadLocalMap对象
第二种情况: map存在, 但是没有与当前ThreadLocal关联的entry
*/
return setInitialValue();
}
/**
* 初始化
*
* @return the initial value 初始化后的值
*/
private T setInitialValue() {
// 调用initialValue获取初始化的值
// 此方法可以被子类重写, 如果不重写默认返回null
T value = initialValue();
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 判断map是否存在
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);
// 返回设置的值value
return value;
}
具体分析下执行流程:
A. 首先获取当前线程, 根据当前线程获取一个Map
B. 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entry e,否则转到D
C. 如果e不为null,则返回e.value,否则转到D
D. Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map
总结: 先获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始值。
再看下remove方法:
/**
* 删除当前线程中保存的ThreadLocal对应的实体entry
*/
public void remove() {
// 获取当前线程对象中维护的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果此map存在
if (m != null)
// 存在则调用map.remove
// 以当前ThreadLocal为key删除对应的实体entry
m.remove(this);
}
/**将key进行移除掉。那么这里对value的处理?
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
e.clear();
// 可以看到这里已经将null也来进行重置
expungeStaleEntry(i);
return;
}
}
}
执行流程:
A. 首先获取当前线程,并根据当前线程获取一个Map
? B. 如果获取的Map不为空,则移除当前ThreadLocal对象对应的entry
再来看下initialValue方法,这个方法一般来说,根据自己的场景来进行重写。
/**
* 返回当前线程对应的ThreadLocal的初始值
* 此方法的第一次调用发生在,当线程通过get方法访问此线程的ThreadLocal值时
* 除非线程先调用了set方法,在这种情况下,initialValue 才不会被这个线程调用。
* 通常情况下,每个线程最多调用一次这个方法。
*
* <p>这个方法仅仅简单的返回null {@code null};
* 如果程序员想ThreadLocal线程局部变量有一个除null以外的初始值,
* 必须通过子类继承{@code ThreadLocal} 的方式去重写此方法
* 通常, 可以通过匿名内部类的方式实现
*
* @return 当前ThreadLocal的初始值
*/
protected T initialValue() {
return null;
}
此方法的作用是 返回该线程局部变量的初始值。
(1) 这个方法是一个延迟调用方法,从上面的代码我们得知,在set方法还未调用而先调用了get方法时才执行,并且仅执行1次。
(2)这个方法缺省实现直接返回一个null。
(3)如果想要一个除null之外的初始值,可以重写此方法。
4、ThreadLocalMap
ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现。
成员变量:
/**
* 初始容量 —— 必须是2的整次幂
*/
private static final int INITIAL_CAPACITY = 16;
/**
* 存放数据的table,Entry类的定义在下面分析
* 同样,数组长度必须是2的整次幂。
*/
private Entry[] table;
/**
* 数组里面entrys的个数,可以用于判断table当前使用量是否超过阈值。
*/
private int size = 0;
/**
* 进行扩容的阈值,表使用量大于它的时候进行扩容。
*/
private int threshold; // Default to 0
存储的基本结构:
/*
* 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来保存K-V结构数据的。不过Entry中的key只能是ThreadLocal对象,这点在构造方法中已经限定死了。
另外,Entry继承WeakReference,也就是key(ThreadLocal)是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑。
ThreadLocal中的set方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocal.ThreadLocalMap map = getMap(t);
if (map != null)
//调用了ThreadLocalMap的set方法
map.set(this, value);
else
createMap(t, value);
}
ThreadLocal.ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
//调用了ThreadLocalMap的构造方法
t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
}
这个方法我们刚才分析过, 其作用是设置当前线程绑定的局部变量 :
A. 首先获取当前线程,并根据当前线程获取一个Map
B. 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)
(这里调用了ThreadLocalMap的set方法)
C. 如果Map为空,则给该线程创建 Map,并设置初始值
(这里调用了ThreadLocalMap的构造方法)
这段代码有两个地方分别涉及到ThreadLocalMap的两个方法, 我们接着分析这两个方法。
/*
* firstKey : 本ThreadLocal实例(this)
* firstValue : 要保存的线程本地变量
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//初始化table
table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
//计算索引(重点代码)
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//设置值
table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
size = 1;
//设置阈值
setThreshold(INITIAL_CAPACITY);
}
5、实际应用
这里如果用到了到时候再回来进行补充。通过一个实际案例来结合分析一波:
/**
* 在多线程条件下,直接将PERSON和每个线程来进行绑定。通过PERSON来操作,实现每个线程之间的隔离性,可以看到利用set/get方法来达到
* 响应的操作目的,而且辖区内城之间的值互相不干扰
*/
@Slf4j
public class ThreadLocalTest2 {
private static final ThreadLocal<Person> THREAD_LOCAL = new ThreadLocal<>();
private static final Person PERSON = new Person();
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
// 对于THREAD_LOCAL来说,二者的set和get可能不是处于同一个地方,但是这里来进行模拟的时候是这样子来进行操作。
THREAD_LOCAL.set(PERSON);
Person person = THREAD_LOCAL.get();
// 这样子来写,直接报错。
// 可以看到这里并不去提供修改绑定的状态来,只是提供了set/get方式来进行使用。使用这个线程绑定,更倾向于利用的是里面的方法,而不是属性
person.setId((int) Thread.currentThread().getId());
person.setUsername(""+((int) Thread.currentThread().getId()-11));
// 可以看到对象是同一个地址值。但是变量的值已经都给改变了
// log.info("person对象的hashcode值是:{}",THREAD_LOCAL.get());
// 尽管地址值是同一个,但是里面的属性已经发生了改变。
log.info("person对象的id值是:{},对应的username值是:{}",THREAD_LOCAL.get().getId(),THREAD_LOCAL.get().getUsername());
THREAD_LOCAL.remove();
}).start();
}
}
}
通过上面的案例可以看到如果我们这么来进行操作,发现对象的地址都是一样的。但是对于对象的属性来说,每个线程中的都不一样。
再次回到第一回讲到的官方API中讲述的:
ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。