ThreadLocal了解吗?
ThreadLocal了解吗?
1、四种引用类型:强、软、弱、虚引用
- 强:如果一个对象具有强引用,那么它永远不会被 GC,当内存空间不足时,JVM 宁愿抛出OutOfMemoryError(OOM)
- 软:用java.lang.ref.SoftReference类来表示。内存充足,垃圾回收器就不会回收它;内存不足,就回收。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。
- 弱:用java.lang.ref.WeakReference类来表示,不管当前内存空间足够与否,都会对它进行回收
- 虚:用java.lang.ref.PhantomReference类表示,不影响对象的生命周期,在任何时候都可能被垃圾回收器回收,永远拿不到虚引用指向的对象
- ps:虚引用必须和引用队列关联使用,虚引用被用来管理JVM中的堆外内存
2、ThreadLocal是什么?
它为线程提供了本地存储,它会为每个线程分别存储一份唯一的数据,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本
3、从数据结构入手

- 每个Thread线程内部都有一个Map
- Map里面存储线程本地对象(key)和线程的变量副本(value)
- 但是,Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
4、ThreadLocal底层原理
当一个线程往ThreadLocal里存放数据时
- 获取当前线程
- 得到当前线程的ThreadLocalMap,如果当前线程的ThreadLocalMap为空,就新建一个
- 把ThreadLocal对象当做key,把数据当做value存放到map中
源码:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value); //在map中以ThreadLocal对象最为key,值作为value
else
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
Thread代码中
ThreadLocal.ThreadLocalMap threadLocals = null;
每创建一个线程,线程就有一个ThreadLocalMap(threadLocals)。当一个线程往ThreadLocal里存放数据时,实际上是存到了自己的ThreadLocalMap里,并且在这个map中,以ThreadLocal对象作为key,以要存的值作为value,这就解释了,为什么一个线程往ThreaLocal里存放数据时,其他的线程无法得到数据。
5、ThreadLocalMap
ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也独立实现。
在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。但是Entry中key只能是ThreadLocal对象,这点被Entry的构造方法已经限定死了。从源码中可以看出,Entry类继承了弱引用类,构造函数中,调用了弱引用类的构造函数,也就是说,Entry对象和ThreadLocal对象之间是弱引用,但只有Key是弱引用类型的,Value并非弱引用。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap的成员变量:
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
/**
* The number of entries in the table.
*/
private int size = 0;
/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0
6、Hash冲突怎么解决
和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。
7、ThreadLocal 内存泄漏问题
内存泄漏:对于应用程序来说,当对象已经不再被使用,但是Java的垃圾回收器不能回收它们的时候,就产生了内存泄露。
我们来看下下面这张图:
上面这张图详细的揭示了ThreadLocal和Thread以及ThreadLocalMap三者的关系。
-
Thread中有一个map,就是ThreadLocalMap
-
ThreadLocalMap的key是ThreadLocal,值是我们自己设定的。
-
ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收
-
重点来了,突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。
解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。
8、ThreadLocal的使用场景
- 线程间数据隔离
- Spring中的声明式事务
- 数据库连接,Session会话管理,会将Connection对象放到ThreadLocal中,这样一个线程中所有方法使用的都是同一个Connection对象,不同线程使用的是不同的Connection对象。