JDK 版本 1.8
ThreadLocal 为每一个使用该变量的线程都提供了独立的副本,可以做到线程间的数据隔离,每一个线程都可以范文各自内部的副本变量。
code.7z
一、ThreadLocal 简单使用案例
二、ThreadLocal#set
分析
public class ThreadLocal<T> {
public void set(T value) {
// ① 获取当前线程的 ThreadLocalMap
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// ② 当前线程的 ThreadLocalMap 存在,直接往里面设值
if (map != null)
map.set(this, value);
else
// ③ 当前线程的 ThreadLocalMap 不存在,创建一个信息的 ThreadLocalMap
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
}
三、ThreadLocal#get
分析
public class ThreadLocal<T> {
public T get() {
// ① 获取当前线程 ThreadLocalMap
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// ② 尝试从 ThreadLocalMap 中获取相关数据,存在直接返回
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// ③ 如果不存在进行值的初始化,同时创建 ThreadLocalMap 到当前线程中
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;
}
}
执行流程如下get
和 set
一样,所有的操作都是对 ThreadLocalMap
的操作
四、ThreadLocalMap
分析
ThreadLocalMap 是 Thread 类中的一个变量,也就是说每个线程都有自己专属的 ThreadLocalMap。 线程上下文的存值操作本质上都是在对自己线程内部的 ThreadLocalMap 做操作,从而实现的线程隔离。
4.1、ThreadLocalMap
数据结构
ThreadLocalMap 通过数组进行数据存储,而数组的结构为:Entry
。
Entry
分析
Entry
的数据结构为 key-value 形式,key 为 ThreadLocal<?> value 为相应的值,相关代码如下:
public class ThreadLocal<T> {
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;
}
}
}
}
小贴士 强引用:通常 new 出来的对象就是强引用,只要强引用存在,垃圾回收器将永远不会被引用的对象,即使内存不足 软引用:使用 SoftReference 修饰的对象被称为 软引用,软引用指向的对象在内存要溢出的时候会被回收 弱引用:使用 WeakReference 修饰的对象被称为 弱引用,只要发生垃圾回收,如果这个对象只被弱引用指向,那么就会被回收 虚引用:虚引用是最弱的引用,使用 PhantomReference 进行定义。
Entry
中的 key 采用的弱引用,而 value 使用的强引用。
小贴士
Entry
key 使用的弱引用,在 gc 后一定会被回收? 当 key 没有被强引用,在 gc 之后,key 会被回收。当 key 被前引用,在 gc 后 key 不会被回收 相关代码操作如下
4.2、ThreadLocalMap Hash 算法
Hash 算法
ThreadLocalMap 中使用
Enrty[]
来存值,同时存在要将值放在哪个数组槽的问题,这时候就需要相关 Hash 算法来实现。
通过 ThreadLocalMap#set
找到相关的 Hash 算法代码如下:
public class ThreadLocal<T> {
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
static class ThreadLocalMap {
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
......
}
}
}
根据上述的算法对长度为 16 的数组,模拟插入 16个数据,看看hash 计算到的数组下标值
通过测试能够发现 hash 之后的数据散列的很均匀,长度为 16 的数组,进行16次散列获取下标值,一次都没有重复过。
不过当存入的值越多势必会存在 冲突的问题,ThreadLocalMap
如何解决 Hash 冲突问题
Hash 的结果存在以下几种情况
第一种:直接 hash 到一个空白的位置,直接插入
逻辑图如下:
对应 ThreadLocalMap#set
部分代码如下
第二种:hash 到一个正常的位置,不过该位置 key 和要存入的值 key 相同,直接更新
逻辑图如下:
对应 ThreadLocalMap#set
部分代码如下
第三种:hash到一个正常的位置,不过该位置 key 不相同
此情况会有四种情况
- 一、向后移动的过程中遇到空的,直接进行存值
对应 ThreadLocalMap#set
部分代码如下
- 二、向后移动过程中遇到相同key的,直接进行更新
对应 ThreadLocalMap#set
部分代码如下
- 三、向后移动的过程中遇到 key = null 的复杂操作
在遇到 key =null 的时候,会通过调用ThreadLocalMap#replaceStaleEntry
完成设值操作
主要操作的步骤有如下图代码:
主要操作如下:
1、向前遍历获取 key =null 最前面的下标值(Entrty 不为空的前提下)
对应 ThreadLocalMap#replaceStaleEntry
代码如下
2、向后遍历尝试寻找相同 key 的位置进行更新
对应 ThreadLocalMap#replaceStaleEntry
代码如下
3、向后遍历如果不存在相同 key 位置时,则直接往该位置进行添加操作,(该位置此时 key=null ,为可以被替换的数据)
对应 ThreadLocalMap#replaceStaleEntry
代码如下
4.3、ThreadLocalMap 如何防止内存泄露
ThreadLocalMap
以 key - value 的方式进行存值,其中 key 是弱引用,value 是强引用,在 key 没有指向强引用时,垃圾回收会将 key 进行回收,此时会出现 key = null value 有值的情况,如果此时 线程对象不被回收,那么 value 就会常驻内存,长期积累便会导致内存泄露问题,对此 ThreadLocalMap
提供了方案来缓解该问题。
1、在 ThreadLocal#get 时,清除已经key被垃圾回收的数据
public class ThreadLocal<T> {
public T get() {
......
ThreadLocalMap.Entry e = map.getEntry(this);
......
return setInitialValue();
}
static class ThreadLocalMap {
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
// 取不到值,进行清除操作
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 查找 key 为 null 的 Entry
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
// 将 key = null 的 Entry 删除
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
}
}
2、在 ThreadLocal#set 时,清除key已经被垃圾回收的数据
public class ThreadLocal<T> {
public void set(T value) {
......
map.set(this, value);
......
}
static class ThreadLocalMap {
private void set(ThreadLocal<?> key, Object value) {
......
if (k == key) {
......
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
......
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
// 查找 key = null 的 Entry
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i); // 删除 key = null 的 Entry
}
} while ( (n >>>= 1) != 0);
return removed;
}
}
}
无论是通过 get
还是 set
方法进行清除 key 被回收的数据,最终采用的是两种请求方式:
ThreadLocalMap#cleanSomeSlots
- ‘ThreadLocalMap#expungeStaleEntry’
ThreadLocalMap#expungeStaleEntry
以 ‘ThreadLocalMap#get’ 中调用到的
ThreadLocalMap#expungeStaleEntry
为例
执行逻辑图如下:ThreadLocalMap#get
在获取不到值,会进行循环遍历,在 下标位置不为空时,一直向后遍历,如果遍历得到就返回,如果遍历不到返回空,再次期间通过 ThreadLocalMap#expungeStaleEntry
进行 key =null 的 Enrty 对应的 value 进行回收。
回收代码如下:
回收的方式是一轮一轮的扫,防止遗漏,在这过程中,如果存在 key 不为 null 但是对应的 hash 值计算不一致的,进行位移处理。
ThreadLocalMap#cleanSomeSlots
以 ‘ThreadLocalMap#set’ 中调用的
ThreadLocalMap#cleanSomeSlots
为例
ThreadLocalMap#cleanSomeSlots
调用时配合 ThreadLocalMap#expungeStaleEntry
进行多次扫描,尽可能保证被回收的key,对应的 value 能够得到及时的回收,尽可能的防止因为 ThreadLocalMap 导致的内存泄露问题。