目录
ThreadLocal介绍
多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。
ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题
常用方法
方法声明 | 描述 |
ThreadLocal() | 创建ThreadLocal对象 |
public void set(T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
ThreadLocal与synchronized的区别
虽然ThreadLocal和synchronized关键字都是用于处理多线程并发访问的问题,不过两者处理问题的角度和思路不同
synchronized | ThreadLocal | |
原理 | 同步机制采用“以时间换空间”的方式,只提供了一份变量,让不同的线程排队访问 | ThreadLocal采用“以空间换时间”的方式,为每一个线程都提供了一份变量的副本,从而实现了访问的互不干扰 |
侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据相互隔离 |
ThreadLocal方案的好处
在一些特定场景下,ThreadLocal有两个突出优势
- 传递数据:保存每个线程保定的数据,在需要的地方可以直接获取,避免参数直接传递带来的代码耦合问题
- 线程隔离:各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失
ThreadLocal的数据结构
Thread
类有一个类型为ThreadLocal.ThreadLocalMap
的实例变量threadLocals
,也就是说每个线程有一个自己的ThreadLocalMap
。
ThreadLocalMap
有自己的独立实现,可以简单地将它的key
视作ThreadLocal
,value
为代码中放入的值(实际上key
并不是ThreadLocal
本身,而是它的一个弱引用)。
每个线程在往ThreadLocal
里放值的时候,都会往自己的ThreadLocalMap
里存,读也是以ThreadLocal
作为引用,在自己的map
里找对应的key
,从而实现了线程隔离。
ThreadLocalMap
有点类似HashMap
的结构,只是HashMap
是由数组+链表实现的,而ThreadLocalMap
中并没有链表结构。
JDK8的设计方案的两个好处
- 每个Map存储的Entry数量变少,哈希冲突会变少
- 当Thread销毁的时候,ThreadLocalMap也随之会销毁,减少内存的使用
ThreadLocal的核心方法源码
除了构造方法之外,ThreadLocal对外暴露的方法有4个
方法声明 | 描述 |
proctected T initialValue() | 返回当前线程局部变量的初始值 |
public void set(T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
set方法
public void set(T value) {
//获取当前线程对象
Thread t = Thread.currentThread();
//获取当前线程对象中维护的ThreadLocal对象
ThreadLocalMap map = getMap(t);
if (map != null)
//存在则设置实体entry
map.set(this, value);
else
//1、当前线程Thread不存在ThreadLocalMap对象
//2、则调用createMap进行ThreadLocalMap对象初始化
//3、并将t(当前线程)和value(t对应的值)作为第一个entry存放到ThreadLocalMap中
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
//这里的this是掉本次方法的threadLocal
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
代码执行流程:
- 首先获取当前线程,并根据当前线程获取一个Map
- 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)(调用了ThreadLocalMap构造方法)
- 如果Map为空,则给该线程创建Map ,并设置初始值(调用了ThreadLocalMap构造方法)
get方法
public T get() {
//获取当前线程对象
Thread t = Thread.currentThread();
//获取当前线程对象中维护的ThreadLocal对象
ThreadLocalMap map = getMap(t);
if (map != null) {
//以当前的ThreadLocal为key。调用getEntry获取对应的存储实体e
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//获取存储实体e对应的value值,即为我们想要的当前线程对应此ThreadLocal的值
T result = (T)e.value;
return result;
}
}
/*
初始化:两种情况执行当前代码
1.map不存在,表示此线程没有维护的ThreadLocalMap对象
2.map存在,但是没有与当前ThreadLocal关联的key
*/
return setInitialValue();
}
//返回初始化后的值
private T setInitialValue() {
//调用initialValue获取初始化的值
//此方法可以被子类重写,如果补充些默认返回null
T value = initialValue();
//获取当前线程对象
Thread t = Thread.currentThread();
//获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
//1、当前线程Thread不存在ThreadLocalMap对象
//2、则调用createMap进行ThreadLocalMap对象初始化
//3、并将t(当前线程)和value(t对应的值)作为第一个entry存放到ThreadLocalMap中
createMap(t, value);
return value;
}
代码执行流程:
- 首先获取当前线程,根据当前线程获取一一个Map
- 如果获取的Map不为空,则在Map中以ThreadLocal的弓|用作为key来在Map中获取对应的Entrye ,否则转到4
- 如果e不为null ,则返回e.value ,否则转到4
- Map为空或者e为空,则通过initialValue函数获取初始值value ,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map
总结:先获取当前线程的ThreadLocalMap变量,如果存在则返回值,不存在则创建并返回初始值
remove方法
//删除当前线程中保存的ThreadLocal对应的实体entry
public void remove() {
//获取当前线程对象中维护的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//存在调用map.remove
//以当前ThreadLocal为key删除对应的实体entry
m.remove(this);
}
代码执行流程:
- 首先获取当前线程,并根据当前线程获取一个map
- 如果获取的map不为空,则移除当前的ThreadLocal对应的entry
initialValue方法
// 返回当前线程对应的ThreadLocal的初始值
protected T initialValue() {
return null;
}
此方法的作用是返回该线程局部变量的初始值
- 这个方法是一一个延迟调用方法,从上面的代码我们得知,在set方法还未调用而先调用了get方法时才执行,并且仅执行1次。
- 这个方法缺省实现直接返回一个nu11。
- 如果想要一一个除null之外的初始值,可以重写此方法。 (备注:该方法是一一个protected的方法,显然是为了让子类覆盖而设计的)
ThreadLocalMap源码分析
基本结构
ThreadLocalMap是ThreadLocal的内部类,没有实现map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现。
1.成员变量
/**
* 初始容量必须是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
跟HashMap类似,INITIAL_CAPACITY代表这个Map的初始容量;
table是一个Entry类型的数组,用于存储数据;
2.存储结构--Entry
/*
Entry继承WeakReference,并且用ThreadLocal作为key.
如果key为null(entry.get() == null),意味着key不再被引用,
因此这时候entry也可以从tab1e中清除。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
在ThreadL ocalMap中,也是用Entry来保存K-V结构数据的。不过Entry中的key只能是ThreadLocal对象, 这点在构造方法中已经限定死了
另外, Entry继承WeakReference ,也就是key ( ThreadLocal )是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑。
弱引用和内存泄漏
有些程序员在使用ThreadLocal的过程中会发现有内存泄漏的情况发生,就猜测这个内存泄漏跟Entry中使用了弱引用的key有关系。这个理解其实是不对的。
我们先来回顾这个问题中涉及的几个名词概念,再来分析问题。
内存泄漏的相关概念
-
Memory overflow:内存溢出,没有足够的内存提供申请者使用。
-
Memory leak:内存泄漏是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出
弱引用相关概念
强引用(Strong Reference):就是我们最常见的普通对象引用,只要强引用指向一个对象,就能表明对象还或者,垃圾回收期就不会回收这种对象。
弱引用(WeakReference):垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间是否足够。都会回收他的内存
如果ThreadLocalMap中的key使用强引用
此时ThreadLocal的内存图(实线表示强引用)
- 假设在业务代码中使用完ThreadLocal , threadLocalRef被回收了。即他俩之间的实现断开
- 但是因为threadLocalMap的Entry强引用了threadLocal ,造成threadLocal无法被回收。
- 在没有手动删除这个Entry以及CurrentThread依然运行的前提下,始终有强引用链threadRef->currentThread-> threadLocalMap->entry ,Entry就不会被回收( Entry中包括了ThreadLocal实例和value) ,导致Entry内存泄漏。
也就是说, ThreadLocalMap中的key使用了强引用,是无法完全避免内存泄漏的。
如果ThreadLocalMap中的key使用强引用
- 同样假设在业务代码中使用完ThreadLocal , threadLocal Ref被回收了。
- 由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例,所以threadlocal就可以顺利被gc回收,此时Entry中的key=null。
- 但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链threadRef-> currentThread-> threadLocalMap->entry->value , value不会被回收,而没有key,导致这块value永远不会被访问到了,导致value内存泄漏。
也就是说, ThreadLocalMap中的key使用了弱引用,也有可能内存泄漏。
出现内存泄漏的真实原因
比较以上两种情况,我们就会发现,内存泄漏的发生跟ThreadLocalMap中的key是否使用弱引用是没有关系的。
那么内存泄漏的的真正原因是什么呢?细心的同学会发现,在以上两种内存泄漏的情况中,都有两个前提:
- 没有手动删除这个Entry
- CurrentThread依然运行
第一点很好理解,只要在使用完ThreadLocal ,调用其remove方法删除对应的Entry ,就能避免内存泄漏。
第二点稍微复杂一点,由于ThreadLocalMap是Thread的一个属性,被当前线程所引用,所以它的生命周期跟Thread一样长。那么在使用完ThreadLocal的使用,如果当前Thread也随之执行结束,ThreadLocalMap自然也会被gc回收,从根源上避免了内存泄漏。
综上,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏。
为什么使用弱引用
根据刚才的分析,我们知道了:无论ThreadLocalMap中的key使用哪种类型弓|用都无法完全避免内存泄漏,跟使用弱引用没有关系。
要避免内存泄漏有两种方式:
- 使用完ThreadLocal ,调用其remove方法删除对应的Entry
- 使用完ThreadLocal ,当前Thread也随之运行结束
相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的。也就是说,只要记得在使用完ThreadLocal及时的调用remove,无论key是强引用还是弱引用都不会有问题。
那么为什么key要用弱引用呢?
事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。
这就意味着使用完ThreadLocal , CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。