每日一句
一个人的成功不取决于他的智慧,而是毅力。
基本介绍
-
ThreadLocal是对Thread内部的局部变量ThreadLocalMap的维护类。当线程持有多个ThreadLocal的操作时,会在ThreadLocalMap中通过key进行寻找。
-
每个Thread里面维护了一个ThreadLocal.ThreadLocalMap变量,底层存储结构为Entry[],ThreadLocal实例作为ThreadLocalMap的key,set/get的值为Map的value,其中,key的引用为弱引用。
- 当执行ThreadLocal.set时,实际是将ThreadLocal对象和值通过key-value的形式放进了Thread中的ThreadLocal.ThreadLocalMap属性中,完成了线程隔离存储,保证了线程安全,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
使用场景
总体概述
ThreadLocal常用来做线程隔离,下面将对ThreadLocal的实现原理、设计理念、内部实现细节(Map、弱引用)、还有ThreadLocal存在的内存泄露问题进行讲解。
作用目的
提供一个线程内公共变量,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度,让线程的本地变量进行隔离。
原理概述
内部结构图
![【Java技术探索】ThreadLocal深入浅出的源码分析(核心源码)]()
引用逻辑图(虚线表示弱引用)
![【Java技术探索】ThreadLocal深入浅出的源码分析(核心源码)]()
原理分析
- 一个线程内可以存多个ThreadLocal对象,存储的位置位于Thread的ThreadLocal.ThreadLocalMap变量,在Thread中有如下变量:
/* ThreadLocal values pertaining to this thread.
* This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap是由ThreadLocal维护的静态内部类,正如代码中注解所说这个变量是由ThreadLocal维护的。
我们在使用ThreadLocal的get()、set()方法时,其实都是调用了ThreadLocalMap类对应的get()、set()方法。
- Thread中的这个变量的初始化通常是在首次调用ThreadLocal的get()、set()方法时进行的。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
- 上述set方法中,首先获取当前线程对象,然后通过getMap方法来获取当前线程中的threadLocals:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
- 如果Thread中的对应属性为null,则创建一个ThreadLocalMap并赋值给Thread:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
- 如果已经存在,则通过ThreadLocalMap的set方法设置值,这里我们可以看到set中key为this,也就是当前ThreadLocal对象,而value值则是我们要存的值。
对应的get方法源码如下:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
可以看到同样通过当前线程,拿到当前线程的threadLocals属性,然后从中获取存储的值并返回。在get的时候,如果Thread中的threadLocals属性未进行初始化,则也会间接调用createMap方法进行初始化操作。
![【Java技术探索】ThreadLocal深入浅出的源码分析(核心源码)]()
数据结构
看一下相关的源码:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
// ...
}
ThreadLoalMap的类图结构如下:
![【Java技术探索】ThreadLocal深入浅出的源码分析(核心源码)]()
这里需要留意的是,ThreadLocalMap类中的Entry对象继承自WeakReference,也就是说它是弱引用。
由于ThreadLocalMaps是延迟创建的,因此在构造时至少要创建一个Entry对象。这里可以从构造方法中看到:
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);
}
上述构造方法,创建了一个默认长度为16的Entry数组,通过hashCode与length位运算确定索引值i。而上面也提到,每个Thread都有一个ThreadLocalMap类型的变量。
至此,结合Thread,我们可以看到整个数据模型如下:
![【Java技术探索】ThreadLocal深入浅出的源码分析(核心源码)]()
hash冲突及解决
这里分三种情况:
计算hash值便会有hash冲突出现,常见的解决方法有:再哈希法、开放地址法、建立公共溢出区、链式地址法等。
内存泄露
ThreadLocal使用不当可能会出现内存泄露,进而可能导致内存溢出。下面我们就来分析一下内存泄露的原因及相关设计思想。
内存引用链路
泄露原因分析
-
正常来说,当Thread执行完会被销毁,Thread.threadLocals指向的ThreadLocalMap实例也随之变为垃圾,它里面存放的Entity也会被回收。这种情况是不会发生内存泄漏的。
-
发生内存泄露的场景一般存在于线程池的情况下。此时,Thread生命周期比较长(存在循环使用),threadLocals引用一直存在,当其存放的ThreadLocal被回收(弱引用生命周期比较短)后,对应的Entity就成了key为null的实例,但value值不会被回收。
- 如果此Entity一直不被get()、set()、remove(),就一直不会被回收,也就发生了内存泄漏。
所以,通常在使用完ThreadLocal后需要调用remove()方法进行内存的清除。
为什么使用弱引用而不是强引用?
从表面上看内存泄漏的根源在于使用了弱引用,但为什么JDK采用了弱引用的实现而不是强引用呢?
先来看ThreadLocalMap类上的一段注释:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
为了协助处理数据比较大并且生命周期比较长的场景,hash table的条目使用了WeakReference作为key。
这跟我们想象的有些不同,弱引用反而是为了解决内存存储问题而专门使用的。
我们先来假设一下,如果key使用强引用,那么在其他持有ThreadLocal引用的对象都回收了,但ThreadLocalMap依旧持有ThreadLocal的强引用,这就导致ThreadLocal不会被回收,从而导致Entry内存泄露。
- 对照一下,弱引用的情况。持有ThreadLocal引用的对象都回收了,ThreadLocalMap持有的是ThreadLocal的弱引用,会被自动回收。
(防止用户获取一些不应该获取的数据,因为数据已经被回收了!)
- 只不过对应的value值,需要在下次调用set/get/remove方法时会被清除。
综合对比会发现,采用弱引用反而多了一层保障,ThreadLocal被清理后key为null,对应的value在下一次ThreadLocalMap调用set、get、remove的时候可能会被清除。
所以,内存泄露的根本原因是是否手动清除操作,而不是弱引用。
常见问题
一个线程内可以存多个ThreadLocal对象,存储的位置位于Thread的ThreadLocal.ThreadLocalMap变量,在Thread中有如下变量:
为什么将Map放在每一个Thread里
应为如果将Map放在ThreadLocal中进行维护,即使使用ConcurrentHashMap减少并发竞争,但在形式上还是存在线程间的竞争,而放在各个线程中独立维护,就十分满足线程隔离的设计理念。
ThreadLocal.ThreadLocalMap 与 HashMap有什么不同
解决hash冲突方法不同
拓展: 解决hash冲突的方式1. 开放定址法 2. 再hash 3. 链地址法 4. 公共溢出区。
扩容机制不同
当ThreadLocal.ThreadLocalMap的size大于数据1/2时,会扩容2倍。
为什么Entry的key存储采用弱引用
当ThreadLocal没有引用时,ThreadLocal.ThreadLocalMap依旧存在于Thread中,而ThreadLocal对应的Entry永远不会被使用到,所以采用了弱引用,当ThreadLocal没有引用时,自动key就被GC回收。
为什么Entry的value存储没有采用弱引用
我们存储的对象除了ThreadLocalMap的Value就没有其他的引用了,value一但是对象的弱引用,GC的时候被回收,对象就无法访问了,这显然不是我们想要的。
如何解决