ThreadLocal 原理 源码分析
目录
ThreadLocal 原理 源码分析
ThreadLocal
ThreadLocal 为解决多线程并发
问题提供了一种新的思路。使用它可以很简洁地编写出优美的多线程程序。当使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程提供独立的变量副本
,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
使用场景
- 存储单个线程上下文信息
- 使变量线程安全:变量既然成为了每个线程内部的局部变量,自然就不会存在并发问题了
- 减少参数传递
总结
ThreadLocal 提供了线程独有的局部变量(本地变量、副本变量)
,可以在整个线程存活的过程中随时取用,且线程之间互不干扰。
- ThreadLocal 里 set 进去的数据,其实是存储在当前 Thread 里的(Thread 也是一个对象)
- 每个 Thread 都有一个属性
threadLocals
,类型为ThreadLocal.ThreadLocalMap
,它本质上是一个自定义的map
- 这个 map 的 entry 是
ThreadLocal.ThreadLocalMap.Entry
,这个 entry 继承自WeakReference<ThreadLocal<T>>
- 这表明:
ThreadLocal<?>
是被弱引用对象引用的对象
,ThreadLocal<T>
(而非通过其 set 进去的数据)会在 gc 时被回收 - 这个 map 其实只是一个数组,map 的 get/set 方法的参数(也即象征意义的 key)是
ThreadLocal<?>
- 数组中存储的值(也即象征意义的 value)即通过
ThreadLocal<?>
的 set 方法传过来的 Object(类型就是里面的泛型) - 由于 map 中对
ThreadLocal<?>
是通过弱引用的方式引用的,所以当ThreadLocal<?>
不再被强引用时,此ThreadLocal<?>
对象就会在 gc 时被回收
使用线程池的问题
使用线程池可以达到线程复用的效果,但是归还线程之前记得清除ThreadLocalMap
,要不然再取出该线程的时候,ThreadLocal
变量还会存在。
内存泄漏问题
为何存在内存泄漏的问题
ThreadLocalMap
中是以弱引用的方式引用的ThreadLocal
,如果一个ThreadLocal
没有外部强引用来引用它,那么 GC 的时候,这个ThreadLocal
就会被回收。ThreadLocal
被回收后,ThreadLocalMap
中就会出现key
为null
的Entry
,此后,这些key
为null
的Entry
的value
就没有办法访问了。- 由于这些
key
为null
的Entry
的value
存在一条强引用链:Thread -> ThreaLocalMap -> Entry -> value
,这就造成这些value
永远无法回收。 - 如果当前线程迟迟不结束的话,就会造成内存泄漏。
如何解决内存泄漏的问题
在调用 ThreadLocal
的 get()、set()、remove()
的时候,都会主动清除掉 ThreadLocalMap
里所有 key
为 null
的 value
。
需要明确,使用 ThreadLocal
肯定是存在内存泄漏的问题的,上面的方案虽然解决了部分场景的内存泄漏问题,但并不彻底!
源码分析
Thread
首先从 Thread
类源码入手:
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null; // 存储普通的线程数据(本地变量)
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; // 可以被子类继承
}
Thread
类中有一个 threadLocals
和一个 inheritableThreadLocals
变量,它们都是 ThreadLocalMap
类型的变量。
默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal
类的 set
或get
方法时才创建它们,实际上调用这两个方法的时候,最终调用的是ThreadLocalMap
类对应的方法。
ThreadLocalMap 是 ThreadLocal 的内部类,本质是一个自定义的 map。
ThreadLocal
public ThreadLocal()
protected T initialValue()
public void set(T value)
public T get()
public void remove()
构造方法
private final int threadLocalHashCode = nextHashCode(); //用来在 map 中找到自己
public ThreadLocal() {}
ThreadLocal 实例的变量只有一个threadLocalHashCode
,用来在 ThreadLocalMap 中找到自己存储的位置
set() 方法
public void set(T value) {
Thread t = Thread.currentThread(); // 获取当前线程
ThreadLocalMap map = t.threadLocals; // 返回当前线程中的成员变量 threadLocals
if (map != null) map.set(this, value); // 注意是将该 ThreadLocal 实例作为key
else t.threadLocals = new ThreadLocalMap(this, value); // 初始化线程中的成员变量,并赋值
}
set
方法很简单,主要是判断ThreadLocalMap
是否存在,然后使用ThreadLocalMap
中的set
方法进行数据处理。具体逻辑在后面剖析ThreadLocalMap
源码时再看。
通过上面的set
方法及下面get
方法可知,通过ThreadLoca
的set
方法存储的对象,都是存储在当前线程对象的ThreadLocalMap
中的,其他线程访问不到,各个线程中通过get
方法访问的是不同的对象。
get() 方法
public T get() {
Thread t = Thread.currentThread(); // 获取当前线程
ThreadLocalMap map = t.threadLocals; // 获取当前线程中的 ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); // 获取 ThreadLocal 对应的键值对
if (e != null) return (T)e.value; // 如果键值对存在,则返回当前 ThreadLocal 对应的值
}
return setInitialValue(); // 如果键值对不存在,则返回 initialValue() 方法指定的初始值
}
If the variable has no value for the current thread, it is first initialized to the value returned by an invocation of the initialValue()
method.
private T setInitialValue() {
T value = initialValue(); // 获取指定的初始值
set(value); // 中间这一块的逻辑和 set 方法完全一样
return value;
}
get
方法的逻辑也很简单,如果通过当前 ThreadLocal
能在 map
中找到非空的 Entry
,则正常返回 Entry
中的值(也即之前通过 set
方法存储的值),否则返回 initialValue()
方法指定的初始值。
核心逻辑依旧在ThreadLocalMap
的getEntry(tl)
方法中,具体逻辑同样在后面剖析ThreadLocalMap
源码时再看。
initialValue() 方法
initialValue()
方法仅在 get
方法中被调用,用于返回 ThreadLocal
对应的初始化值,一般作为匿名内部类使用。
关于 initialValue()
的注意事项:
- Returns the current thread's "initial value" for this thread-local variable.
- This method will be invoked the first time a thread accesses the variable with the
get
method, unless the thread previously invoked theset
method, in which case the initialValue() method will not be invoked for the thread. - 注意这个方法可能不调用,也可能调用多次,不能在里面做初始化逻辑
一般没啥用的一个方法
ThreadLocalMap
ThreadLocalMap
类似HashMap
的结构,只是HashMap
是由数组+链表实现的,而ThreadLocalMap
中并没有链表结构。
ThreadLocalMap is a
customized hash map
suitable 适用于 only for maintaining 维护 thread local values.
基础结构
static class ThreadLocalMap {
//The table, resized as necessary. table.length MUST always be a power of two.
private Entry[] table; // 可自动扩容的数组
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value; // The value associated with this ThreadLocal
Entry(ThreadLocal<?> k, Object v) {
super(k); // key
value = v; // value
}
}
//...
}
- ThreadLocalMaps are constructed
lazily
, so we only create one when we have at least one entry toput
in it. - To help deal with very large and long-lived 长期存在 usages, the hash table entries use
WeakReferences
forkeys
. - However, since
ReferenceQueue
are not used, stale 过时的 entries are guaranteed 确保 to be removed only when the table starts running out of space 没有空间. - Note that
null keys
mean that the key is no longer referenced, so the entry can be expunged 移除 from table.
Hash 算法
int i = key.threadLocalHashCode & (table.len-1); // 计算当前 key 在散列表中对应的数组下标位置
这里最关键的就是threadLocalHashCode
值的计算,这个值其实在创建 ThreadLocal 时已经计算好了。
private final int threadLocalHashCode = nextHashCode(); // ThreadLocal 的 hash 值
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT); // Atomically adds the given value to the current value.
}
private static AtomicInteger nextHashCode = new AtomicInteger(); //The next hash code to be given out. Updated atomically. Starts at zero.
private static final int HASH_INCREMENT = 0x61c88647;
每当创建一个ThreadLocal
对象,这个ThreadLocal.nextHashCode
这个值就会在之前的基础上增长 0x61c88647
。这个值很特殊,它是斐波那契数也叫黄金分割数,hash 增量为这个数字的好处就是:hash 分布非常均匀。
Hash 冲突
HashMap
中解决冲突的方法是链表法
,冲突的数据挂载到链表上,如果链表长度超过一定数量则会转化成红黑树。
ThreadLocalMap
中解决冲突的方法是开放地址法
(线性探测法
):插入一个Entry
时,如果通过hash
计算的槽位中已经有了Entry
数据,此时就会线性向后查找,一直找到Entry
为null
的槽位才会停止查找,并将当前元素放入此槽位中
set() 方法
往ThreadLocalMap
中set
数据分为以下几种情况:
- 通过
hash
计算后的槽位对应的Entry
数据为空:直接将数据放到该槽位即可 - 通过
hash
计算后的槽位对应的Entry
数据不为空- 如果
key
值与当前ThreadLocal
通过hash
计算获取的key
值一致:更新该槽位的数据即可 - 如果
key
值与当前ThreadLocal
通过hash
计算获取的key
值不一致:线性向后查找- 往后遍历过程中,在找到
Entry
为null
的槽位之前,没有遇到key
过期的Entry
:遍历散列数组,线性往后查找,如果找到Entry
为null
的槽位,则将数据放入该槽位中,或者往后遍历过程中,遇到了key
值相等的数据,直接更新即可 - 往后遍历过程中,在找到
Entry
为null
的槽位之前,遇到了key
过期的Entry
:会进行一轮探测式清理操作,具体逻辑就不去理了,意义不大
- 往后遍历过程中,在找到
- 如果
在set()
方法其实做了很多事情,包括:添加数据、更新数据、清理数据、优化数据桶的位置、数组扩容,具体逻辑就不去理了,意义不大。
get() 方法
get
的逻辑和set
类似,分为以下几种情况:
- 通过
hash
计算后的槽位对应的Entry.key
和查找的key
一致:则直接返回 - 通过
hash
计算后的槽位对应的Entry.key
和查找的key
不一致:则往后迭代查找,查找过程中也会进行一轮探测式清理操作
InheritableThreadLocal
使用ThreadLocal
的时候,在异步场景下是无法给子线程
共享父线程
中创建的线程副本数据的。使用InheritableThreadLocal
便可以解决这个问题。
实现原理
实现原理很简单:在父线程中通过new Thread()
创建子线程时,Thread#init
方法会在Thread
的构造方法中被调用,在init
方法中会拷贝父线程的InheritableThreadLocal
中的数据到子线程中:
private void init(...boolean inheritThreadLocals) {
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
测试代码
ThreadLocal<String> threadLocal = new ThreadLocal<>();
ThreadLocal<String> local = new InheritableThreadLocal<>();
threadLocal.set("白乾涛");
local.set("白乾涛");
ThreadLocal<String> threadLocal2 = ThreadLocal.withInitial(() -> "白乾涛2");
ThreadLocal<String> local2 = InheritableThreadLocal.withInitial(() -> "白乾涛2");
new Thread(() -> {
System.out.println("子线程获取父线程ThreadLocal数据:" + threadLocal.get() + "-" + threadLocal2.get()); // null-白乾涛2
System.out.println("子线程获取父线程InheritableThreadLocal数据:" + local.get() + "-" + local2.get()); // 白乾涛-白乾涛2
}).start();
案例
public class Test {
ThreadLocal<String> mNameLocal = new ThreadLocal<>();
ThreadLocal<Long> mIdLocal = ThreadLocal.withInitial(() -> {
System.out.println(Thread.currentThread().getName() + " 调用了 get");
return -1L; //初始化数据
});
public static void main(String[] args) throws InterruptedException {
new Test().test();
}
private void test() throws InterruptedException {
mNameLocal.set(Thread.currentThread().getName()); //更新【主线程】数据
mIdLocal.set(Thread.currentThread().getId()); //更新【主线程】数据
Thread thread = new Thread(() -> {
mNameLocal.set(Thread.currentThread().getName()); //更新【子线程】数据
//mIdLocal.set(Thread.currentThread().getId()); //更新【子线程】数据
System.out.println(mNameLocal.get() + " " + mIdLocal.get()); //Thread-0 -1
});
thread.start();
thread.join(); //效果等同于同步:等 thread 执行完毕后再执行下面的逻辑
System.out.println(mNameLocal.get() + " " + mIdLocal.get()); //main 1
mNameLocal.remove();
mIdLocal.remove();
System.out.println(mNameLocal.get() + " " + mIdLocal.get()); //null -1
}
}
打印结果:
Thread-0 调用了 get
Thread-0 -1
main 1
main 调用了 get
null -1
2017-07-29
本文来自博客园,作者:白乾涛,转载请注明原文链接:https://www.cnblogs.com/baiqiantao/p/7257326.html