关于ThreadLocal的那些事
- 那么,ThreadLocal有什么故事呢?
- 他的起源
- 剖析
- 他的用例
?看到这里,发现ThreadLocal好像故事不多,确实,他的故事并不够多,但是,他的每个故事,都沁人心脾。
一.ThreadLocal的起源
1.ThreadLocal是什么
ThreadLocal是一个类,采用ThreadLocal声明的变量也称为线程本地变量。ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。即线程资源绑定。
ThreadLocal是连接ThreadLocalMap和Thread
的桥梁,来处理Thread的TheadLocalMap属性,包括init初始化属性赋值、get对应的变量,set设置变量等。通过当前线程,获取线程上的ThreadLocalMap属性,对数据进行get、set等操作。
//以下是在Thread.java中的TheadLocalMap
//与此线程相关的threadLocal的值,这个map由ThreadLocal维护
ThreadLocal.ThreadLocalMap threadLocals = null;
//与此线程相关的InheritableThreadLocal值,此map由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
通过上述代码,我们大概可以猜到,为什么ThreadLocal能够实现线程的本地变量,就是因为在线程类中声明了ThreadLocalMap的字段,将Thread
与ThreadLocalMap
进行了绑定。
2.他来自哪里
ThreadLocal是JDK1.2起定义的一个类。
/**
* 该类提供线程局部变量。该局部变量不同于正常的实例局部变量,
* 而是独属于线程的。
* 常用于:一个线程持有一个UserId、或者一个事务ID、一个会话等等。
* 由于是每个线程独立且私有的状态,因此最好设置为private static。
* <p>每个线程都拥有对其本地线程副本的隐式引用
* 变量只要线程处于活动状态且{@code ThreadLocal}
* 实例可访问;一个线程消失后,它的所有副本
* 线程局部实例受垃圾回收(除非其他存在对这些副本的引用)
*/
public class ThreadLocal<T> {
//里面的内容接下来会讲解
}
以下是ThreadLocalMap,该类是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;
}
}
}
这个ThreadLocalMap不是继承于Map,而是内部定义了一个Entry,该类继承弱引用,即将ThreadLocal作为键值,而value即为变量的值。
二.剖析源码
先来看看ThreadLocal提供的几个操作方法:
- 先对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();
}
//再看getMap(t)
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可以看到,get()方法做的事情就是:
- 从当前线程中获取ThreadLocalMap;
- 从ThreadLocalMap中取出entry;
- 再从entry中取出value(变量对象或者变量值);
- 如果map或者map中的entry为空,则初始化一个值。
- 解析完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);
}
//解析如上代码还需要解析createMap(t, value);和map.set(this, value);
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
对上面的两个方法进行逐个解析:
- 获取当前线程的threadLocalMap,然后直接将 ThreadLocal 对象(即代码中的 this)与目标实例的映射添加进 ThreadLocalMap 中。当然,如果映射已经存在,就直接覆盖。(即每个线程在对同一个threadLocal变量调用set()方法,是一个覆盖的过程)
- 如果threadLocalMap为空,则创建新的map,创建新的map则是通过new 一个threadLocalmap给当前线程的threadLocals。
接下来,是一个重点,在很多的讲解教程中,许多作者会有意无意地忽略这个方法,这是一个难点,只有理解了这个方法,才能真正理解ThreadLocal。
方法如下,这个方法的解析采用注释进行解析,读者只需要跟着注释理解就可以了:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;//table为map中的entry[]
int len = tab.length;
//计算出这个ThreadLocal变量的索引;
int i = key.threadLocalHashCode & (len - 1);
//可能有人会疑惑这里为什么使用一个循环,而不是直接进行索引后覆盖
//原因:ThreadLocal变量可能会存在哈希冲突,因此需要进行寻址判断。
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//这里就是实现同个threadLocal变量下的值覆盖。
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();
}
}
好了,在理解代码注释过后,我们再进行全局的解读:
1.我们回溯到上一层的方法调用,是在ThreadLocal进行一个set()方法的调用,如果map存在的情况下,就调用上述方法;
2.重点难点来了:为什么一个线程只维护一个threadLocalMap,而一个线程却可以持有多个ThreadLocal定义的对象?
2.1ThreadLocalMap中维护了一个Entry[] table
,默认容量为16;达到2/3会扩容;
2.2每个entry实体由<ThreadLocal,Object>
组成;
2.3因此,由每个entry实体的KEY不同(KEY即ThreadLocal变量),从而一个线程可以持有一个ThreadLocalMap,一个ThreadLocalMap可以保存多个Entry<?,?>,一个entry实体持有一个ThreadLocal定义的对象。
3.从以上的set()方法,我们可以看到,相同的key的value会被覆盖,因此,一个线程要持有多个本地变量,就需要用ThreadLocal
定义多个对象。
- 解析完set(),我们接着解析初始化值的方法:
protected T initialValue() {
return null;
}
可以看到默认的初始化值方法是空的,因此,再进行查找,发现:
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
return value;
}
以上方法与set()基本一致,无非就是将初始化值添加进threadLocalMap。
- 接下来再解析一下
remove()
方法:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
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();
expungeStaleEntry(i);
return;
}
}
}
以上方法由ThreadLocal调用,获取当前线程的threadLocalMap,移除当前线程下的threadLocalMap中的对于的entry实例,并非移除所有entry。
- 以上的操作解析已经完毕,接下来再讲讲内存回收与内存泄漏吧。
- 内存回收可以分为两个层面:
-
ThreadLocal 层面的内存回收:
当线程死亡时,那么所有的保存在的线程局部变量就会被回收,其实这里是指线程Thread对象中的 ThreadLocal.ThreadLocalMap threadLocals 会被回收,这是显然的。 -
ThreadLocalMap 层面的内存回收:
如果线程可以活很长的时间,并且该线程保存的线程局部变量有很多(也就是 Entry 对象很多),那么就涉及到在线程的生命期内如何回收 ThreadLocalMap 的内存了,不然的话,Entry对象越多,那么ThreadLocalMap 就会越来越大,占用的内存就会越来越多,所以对于已经不需要了的线程局部变量,就应该清理掉其对应的Entry对象。
-
使用的方式是,Entry对象的key是WeakReference 的包装,当ThreadLocalMap 的 private Entry[] table,已经被占用达到了三分之二时 threshold = 2/3(也就是默认情况下线程拥有的局部变量超过了10个) ,就会尝试回收 Entry 对象,我们可以看到 ThreadLocalMap.set()方法中有下面的代码:
if (!cleanSomeSlots(i, sz) && sz >= threshold) {
rehash();
}
- 接下来讲讲内存泄漏:
每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收。但是,此时的value却不能回收,因为存在一条从current thread连接过来的强引用,只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收。所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束之间的这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的就可能出现内存泄露 。(这种问题在web的多线程容器极其常见,多线程容器,正常配置都是采用线程池,每个请求都从线程池中启用一个线程进行处理,使用完之后线程没有销毁,就会发生内存泄漏)
图示如下:
因此,使用的时候记得手动执行remove方法!
好了,内存方面的问题讲解到此结束!
三.用例
1.测试用例
以下是一个测试用例:
import java.util.concurrent.atomic.AtomicInteger;
import java.lang.ThreadLocal;
/**
* @author linxu
* @date 2019/3/27
* 这是一个线程ID类,采用原子自增,为每个访问的线程赋予一个ID,相同线程访问ID相 * 同。
*/
public class ThreadId {
// 包含要分配的下一个线程ID的原子整数
private static final AtomicInteger nextId = new AtomicInteger(0);
// Thread local variable containing each thread's ID
// 包含每个线程ID的线程局部常变量
private static final ThreadLocal<Integer> threadId =
new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
/**
*
* @return 返回当前线程的唯一ID,必要时进行分配
*/
public static int get() {
return threadId.get();
}
//这里省略了一个公开的remove方法。
}
主函数方法:
public class ThreadLocalTest {
public static void main(String[] args) {
testThreadId();
}
/**
* 测试线程ID,可以看到,相同线程当运行两次的时候,它的ID是不会增加的;
* 如下结果:
* 当前线程:pool-1-thread-1,线程ID为:0
* 当前线程:pool-1-thread-1,线程ID为:0
* 当前线程:pool-1-thread-2,线程ID为:1
* 当前线程:pool-1-thread-3,线程ID为:2
*
*/
private static void testThreadId() {
ExecutorService pool = new ThreadPoolExecutor(3, 5, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(1024));
Runnable target = () -> {
System.err.println("当前线程:" + Thread.currentThread().getName() + ",线程ID为:" + ThreadId.get());
};
for (int i = 0; i < 4; i++) {
pool.execute(target);
}
pool.shutdown();
}
}
其实用例有很多,比如在线程之间传递数据,避免在每个方法上传入参数等等;又或者数据库会话Session或者连接Connection,事务等等,每个线程都持有自己的本地数据,避免互相干扰。
总结
(1)ThreadLocal只是操作Thread中的ThreadLocalMap对象的集合;
(2)ThreadLocalMap变量属于线程的内部属性,不同的线程拥有完全不同的ThreadLocalMap变量;
(3)线程中的ThreadLocalMap变量的值是在ThreadLocal对象进行set或者get操作时创建的;
(4)使用当前线程的ThreadLocalMap的关键在于使用当前的ThreadLocal的实例作为key来存储value值;
(5) ThreadLocal模式至少从两个方面完成了数据访问隔离,即纵向隔离(线程与线程之间的ThreadLocalMap不同)和横向隔离(不同的ThreadLocal实例之间的互相隔离);
(6)一个线程中的所有的局部变量其实存储在该线程自己的同一个map属性中;
(7)线程死亡时,线程局部变量会自动回收内存;
(8)线程局部变量时通过一个 Entry 保存在map中,该Entry 的key是一个 WeakReference包装的ThreadLocal, value为线程局部变量,key 到 value 的映射是通过:ThreadLocal.threadLocalHashCode & (INITIAL_CAPACITY - 1) 来完成的;
(9)当线程拥有的局部变量超过了容量的2/3(没有扩大容量时是10个),会涉及到ThreadLocalMap中Entry的回收;
对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。