文章目录
ThreadLocal
一、ThreadLocal 概述
1、概述
ThreadLocal 是 Java 中提供的一种用于实现线程局部变量的工具。它允许每个线程都拥有自己的独立副本,从而实现线程隔离。ThreadLocal 可以用于解决多线程中共享对象的线程安全问题。
通常我们会使用 synchronzed 或 lock 来控制线程对临界区的访问,区别在于:
- synchronized 和 lock 是通过加锁,牺牲时间来解决访问冲突。
- ThreadLocal 是通过每个线程都拥有自己的“共享资源”,牺牲空间来解决冲突。
所以 ThreadLocal 就是通过空间换时间,虽然内存占用变大了,但是减少了阻塞,提高了效率。
2、ThreadLocalMap
ThreadLocal
有一个静态内部类 ThreadLocalMap
public class ThreadLocal<T> {
static class ThreadLocalMap {
// 底层存数据的数组
private Entry[] table;
// 初始容量(必须是2的幂,默认为16)
private static final int INITIAL_CAPACITY = 16;
// table中Entry的数量
private int size = 0;
// 扩容阈值,当size达到阈值时会触发扩容(loadFactor=2/3;newCapacity=2*oldCapacity)
private int threshold; // Default to 0
// 参数:ThreadLocal对象 和 对应的value
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化table数组
table = new Entry[INITIAL_CAPACITY];
// 根据 ThreadLocal对象 确定 数组下标
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 将 ThreadLocal对象 和 对应的value 存到数组指定的下标处
table[i] = new Entry(firstKey, firstValue);
// 初始化为1
size = 1;
// 设置扩容阈值(数组长度的2/3)
setThreshold(INITIAL_CAPACITY);
}
// 设置扩容阈值(数组长度的2/3)
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
}
}
3、ThreadLocalMap - Entry
ThreadLocalMap
本质是一个 Entry
类型的数组 table
,而Entry
继承于WeakReference
弱引用。
public class ThreadLocal<T> {
static class ThreadLocalMap {
private Entry[] table;
/**
* Entry本质是弱引用
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // ThreadLocal对象是弱引用
value = v; // ThreadLocal的值是强引用
}
}
}
}
实例化Entry
时,将 ThreadLocal对象
传给父类 WeakReference
的构造器
public class WeakReference<T> extends Reference<T> {
// ThreadLocal建立弱引用
public WeakReference(T referent) {
super(referent);
}
}
WeakReference
构造器又将 ThreadLocal对象
传给它的父类Reference
的构造器,赋给 referent
属性
public abstract class Reference<T> {
private T referent;
Reference(T referent) {
this(referent, null);
}
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent; // ThreadLocal
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
}
4、工作原理
每个线程(Thread
)都持有一个 ThreadLocalMap
类型的实例 threadLocals
。
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
工作原理:
ThreadLocalMap
为每个Thread
都维护了一个Entry
数组(保存ThreadLocal
对象 和 对应的value
)- 根据
ThreadLocal
确定Entry
数组的下标,从而可以找到对应的value
。 - 对
ThreadLocal
的操作,其实都是在操作ThreadLocalMap
中的Entry
数组。
5、关联关系图
其中连线5是弱引用,其他都是强引用。
6、使用示例
public class ThreadLocalDemo {
// 创建一个 ThreadLocal 实例
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
// 在主线程中设置线程本地变量的值
threadLocal.set("Main Thread Value");
// 创建并启动两个子线程
new Thread(() -> {
// 在子线程中获取线程本地变量的值
String value = threadLocal.get();
System.out.println("Thread 1 Value: " + value);
}).start();
new Thread(() -> {
// 在另一个子线程中设置线程本地变量的值
threadLocal.set("Thread 2 Value");
// 在子线程中获取线程本地变量的值
String value = threadLocal.get();
System.out.println("Thread 2 Value: " + value);
}).start();
TimeUnit.SECONDS.sleep(1);
// 在主线程中获取线程本地变量的值
String mainThreadValue = threadLocal.get();
System.out.println("Main Thread Value: " + mainThreadValue);
}
}
代码结果如下所示:
Thread 1 Value: null
Thread 2 Value: Thread 2 Value
Main Thread Value: Main Thread Value
7、应用场景
ThreadLocal 的使用场景非常多,比如说:
- 用于保存用户登录信息,这样在同一个线程中的任何地方都可以获取到登录信息。
- 用于保存数据库连接、Session 对象等,这样在同一个线程中的任何地方都可以获取到数据库连接、Session 对象等。
- 用于保存事务上下文,这样在同一个线程中的任何地方都可以获取到事务上下文。
- 用于保存线程中的变量,这样在同一个线程中的任何地方都可以获取到线程中的变量。
例如:通过 Spring MVC 拦截器 将 用户信息 保存到 ThreadLocal 中
public class UserHelper {
private static TransmittableThreadLocal<UserInfo> USER_INFO = new TransmittableThreadLocal<>();
public static UserInfo getUserInfo() {
return USER_INFO.get();
}
public static void setUserInfo(UserInfo userinfo) {
USER_INFO.set(userinfo);
}
public static void clear() {
USER_INFO.remove();
}
}
@Slf4j
public class UserInfoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
try {
String staffId = request.getHeader("STAFF_ID");
String tenantId = request.getHeader("TENANT_ID");
UserInfo userInfo = new UserInfo(staffId, tenantId);
UserHelper.setUserInfo(userInfo);
} catch (Exception e) {
log.error("get userInfo error", e);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
UserHelper.clear();
}
}
二、线性探测算法
1、哈希冲突
哈希表是基于数组的,每个数组元素被称为一个“桶”(Bucket),桶中存储了键值对(Key-Value Pair),键是通过哈希函数生成的,理想的哈希函数可以均匀分布键,从而最大限度地减少冲突。
理想的哈希函数可以均匀分布键,从而最大限度地减少冲突。当两个或多个键的哈希值相同(即映射到同一个桶)时,称之为哈希冲突。常见的解决策略有拉链法
(HashMap)和开放地址法
(ThreadLocalMap)。
和HashMap
不同,ThreadLocalMap
是使用开放地址法
来处理哈希冲突的,主要是因为:
ThreadLocalMap
中的哈希值分散的比较均匀,很少会出现冲突。ThreadLocalMap
经常需要清除无用的对象,冲突的概率就更小了。
2、开放地址法
开放地址法中,若数据不能直接存放在哈希函数计算出来的数组下标时,就需要寻找其他位置来存放。在开放地址法中有三种方式来寻找其他的位置,分别是「线性探测」、「二次探测」、「再哈希法」。
-
线性探测:当哈希函数计算出来的数组下标已经被占用时,就顺序往后查找,直到找到一个空闲的位置。
-
二次探测:当哈希函数计算出来的数组下标已经被占用时,就按照某种规律往后查找,直到找到一个空闲的位置。
比如说每次查找的步长是 1,2,4,8,16……
-
再哈希法:当哈希函数计算出来的数组下标已经被占用时,就使用另一个哈希函数计算出来的数组下标。
3、开放地址法 - 线性探测
1)插入
有一个长度为8的数组,选择的hash函数是 e.key%8,这个8是指数组的长度(容量),当数组长度发生变化,hash函数也应该变化。
插入一个元素e1,key为5,hash函数计算出应该存到位置5(5%8=5)。发现位置5没有被占用,则将e1存入位置5:
又插入一个新元素e2,key为13,hash函数计算出也应该存到位置5(13%8=5),但是发现位置5已经被占用了,此时就往后看位置6有没有被占用,此时发现位置6没有被占用,则e2就存到位置6了
此时又插入一个新元素e3,key为21,发现还要存到位置5,但是位置5已经被占了,往后看位置6,发现位置6也被占了,再看位置7,位置7空着,所以e3就存到位置7了
此时又插入一个新元素e4,key为29,发现还是存到位置5,并且位置5、6、7已经都被占用了,此时只能从头考虑位置0了,发现位置0未被占用,则将e4存到位置0
2)查找
现在要查询key为5的元素,通过计算,对应的位置为5,查看位置5的key,发现位置5的key与要查找的key相等,则查找成功,返回e1;
如果要查询一个key为13的元素,通过计算key为13,对应位置5,但是位置5的key为5,与13不匹配,此时往后看位置6,发现位置6的key为13,与要查找的key相等,此时查找成功,返回e2即可;
如果要查询key为37的元素,通过计算对应位置5,但是位置5的key为5,与13不匹配,往后看位置6的key为13也和37不匹配,一直到位置0,发现e4的key为29,仍旧不匹配要找的37,接着看位置1,发现位置1没有元素,证明数组中没有存key为37的元素,查找失败。
3)删除
如果要删除一个key为13的元素,通过计算key为13,对应位置5,但是位置5的key为5,与13不匹配,此时往后看位置6,发现位置6的key为13,与要删除的key相等,于是将位置6的元素删除。
如果此时不进行其他修改操作,而是进行查找操作,比如查找key为21的元素,应该对应位置5,但是位置5已经有元素了,且不是要找的元素,此时会往后看下一个位置,发现位置6为空,没有元素,所以此次查询失败!!!
但是,这次查询是失败的!不是说查询的方式有问题,而是说数组的元素存放有问题,因为key为21的元素在数组中是有的,但是却并没有被查询出来。
为了解决这个问题,我们在删除元素后,要将其后面的元素进行重新确定位置,也就是rehash,过程如下:
删除的是位置6的元素,所以看位置6后面的元素,7->0->5,每个元素都需要计算hash,确定新位置。
比如位置7的key为21,发现应该调整到位置5,发现位置5已经有了元素,看位置6,发现位置6空着,则将元素e3放入位置6
接着看位置0的元素是否需要调整,在进行计算并且经过上面的流程后,e4应该调整到位置7
需要注意的是,调整位置0后,由于位置1没有元素,则可以停止调整。
- 因为没有元素,则表示后面的第一个非空位置存的元素(比如e1)肯定没有冲突。
4)扩容
当数组中所有位置都填满了,此时再插入元素,就无处安放了,此时有两种做法:1.拒绝插入;2.扩容。
一般来说,并不是当数组没有空位时才扩容,而是数组元素达到一定阈值后就进行扩容,但是需要注意的是数组扩容要做的不只是数组扩容,还需要将旧数组中的元素拷贝到新数组中。
当数组扩容后(假设是翻倍),则数组长度变为16,下标从0~15,如下图所示:
在拷贝的过程中,从左往右遍历旧数组当中的元素,重新计算每个元素的hash值(扩容后,新的散列函数为 e.key%16),也就是确定每个元素在新数组中的位置,依次插入到新数组中,有冲突就按照原来的方式解决冲突即可。
- 首先看位置0,有元素e4,key为29,则新位置为13(29%16=13),发现新数组的位置13空着,于是e4元素就放入位置13;
- 然后从左往右遍历到元素e1,key为5,所以新位置为5(5%16=5),刚好位置5也空着,所以e1放入位置5;
- 然后轮到元素e2,key为13,所以新位置为13(13%16=13),但是位置13上已经有了元素e4,往后看位置14,发现位置14空着,于是e2就放入位置14
- 最后是元素e3,key为21,所以新位置为5(21%16=5),但是位置5上已经有了元素e1,往后看位置6,发现位置6空着,于是e3就放入位置6
所有元素都完成拷贝后,数组的扩容才真的完成,如下图所示:
三、源码分析 - ThreadLocal
对
ThreadLocal
的操作,其实都是在操作ThreadLocalMap
中的Entry
数组。
1、构造方法
ThreadLocal
只有一个无参构造器。
public class ThreadLocal<T> {
public ThreadLocal() {}
}
2、initialValue 方法
如果需要指定默认值,可以重写initialValue
方法。
public class ThreadLocal<T> {
protected T initialValue() {
return null;
}
}
但是要注意:initialValue()
方法并不是在 ThreadLocal
对象创建的时候执行的,而是延迟执行(具体看一下get方法)
- 当调用
ThreadLocal
的get
方法没有获取到值时,才会触发重写的initialValue
方法,设置初始值并返回。
3、set 方法
ThreadLocal
的 set
方法,本质是调用 ThreadLocalMap
的 set
方法
public class ThreadLocal<T> {
public void set(T value) {
// 获取 当前线程 的 ThreadLocalMap
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
// 存在 -> 调用 ThreadLocalMap 的 set方法
map.set(this, value);
else
// 不存在 -> 初始化 ThreadLocalMap,并赋给 Thread 中的 threadLocals
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 初始化ThreadLocalMap,并赋给 Thread 中的 threadLocals
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
}
4、get 方法
public class ThreadLocal<T> {
public T get() {
// 获取 当前线程 的 ThreadLocalMap
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 存在 -> 调用 ThreadLocalMap 的 getEntry方法
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
// 返回 Entry 的 value(即ThreadLocal的值)
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 设置初始值 并 返回初始值
return setInitialValue();
}
private T setInitialValue() {
// 获取初始值
T value = initialValue();
// 下面和 ThreadLocal 的 set方法一致
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
// 返回初始值
return value;
}
// 通过重写该方法指定默认值
protected T initialValue() {
return null;
}
}
可以看到,initialValue()
方法并不是在 ThreadLocal
对象创建的时候执行的,而是延迟执行:
- 当调用
ThreadLocal
的get
方法没有获取到值时,才会触发重写的initialValue
方法,设置初始值并返回。
5、remove 方法
public void remove() {
// 获取 当前线程 的 ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
// 存在 -> 调用 ThreadLocalMap 的 remove方法
m.remove(this);
}
一般是在ThreadLocal
对象使用完后,调用ThreadLocal
的remove
方法,在一定程度上,可以避免内存泄露;
四、源码分析 - ThreadLocalMap
1、set 方法
set()
方法就是根据 ThreadLocal
计算出数组下标 i
,然后将值存储到 table[i]
中。
// java.lang.ThreadLocal.ThreadLocalMap
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 计算新元素应该放到哪个位置(这个位置不一定是最终存放的位置,因为可能会出现hash冲突)
int i = key.threadLocalHashCode & (len - 1);
// 判断计算出来的位置是否存在Entry,如果存在:
// 1. 如果key就是当前的ThreadLocal,则直接更新value
// 2. 如果key为null,说明是陈旧的Entry,则直接构建新的Entry替换之
// 3. 如果key是其他的ThreadLocal,则使用「开放地址法-线性探测」往后找
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
// 获取Entry的key(即 ThreadLocal对象)
ThreadLocal<?> k = e.get();
// 判断key是否相等(判断弱引用的是否为同一个ThreadLocal对象)如果是,则更新value
if (k == key) {
e.value = value;
return;
}
// k为null,说明ThreadLocal对象已经被GC了,当前的Entry是陈旧的(stale entry)
if (k == null) {
// 用新Entry替换旧Entry,同时也会清理其他陈旧的Entry,防止内存泄露
replaceStaleEntry(key, value, i);
return;
}
}
// 走到这,说明数组中没有可以复用的Entry(key就是当前ThreadLocal对象 或者 key为null)
// 创建一个新的Entry,放入数组中
tab[i] = new Entry(key, value);
// Entry数组的元素数量加1
int sz = ++size;
// 先清理一些Slot,防止内存泄漏。
// 1. 如果清理到了,则不需要扩容
// 2. 如果没有清理,由于新插入了一个元素,需要判断是否需要扩容(达到阈值)
if (!cleanSomeSlots(i, sz) && sz >= threshold) {
// 先遍历整个Entry数组清除陈旧Entry,如果还是不行,就扩容(扩容为2倍)
rehash();
}
}
1)replaceStaleEntry
// java.lang.ThreadLocal.ThreadLocalMap
// 用新Entry替换旧Entry,同时也会清理其他陈旧的Entry,防止内存泄露
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// Find either the key or trailing null slot of run, whichever
// occurs first
for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
2)cleanSomeSlots
// java.lang.ThreadLocal.ThreadLocalMap
/**
* 清理一些Slot
*
* @param i:ThreadLocal的下标
* @param n:Entry数组的元素个数
* @return:是否清除了陈旧的entity
*/
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
// entity不为null 但是 key为null -> 陈旧的entity
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i); // 删除陈旧的entity
}
} while ( (n >>>= 1) != 0);
return removed;
}
3)rehash
// java.lang.ThreadLocal.ThreadLocalMap
private void rehash() {
// 遍历整个Entry数组,删除陈旧的Entry
expungeStaleEntries();
// 清除完陈旧Entry后,再次判断是否需要扩容
if (size >= threshold - threshold / 4) {
// 扩容为原来的2倍,遍历将所有元素拷贝到扩容后的新数组中
resize();
}
}
【相关方法】
/**
* 遍历整个Entry数组,删除陈旧的Entry
*/
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
/**
* Double the capacity of the table.
*/
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2; // 新数组为原数组的2倍
Entry[] newTab = new Entry[newLen];
int count = 0;
// 遍历将所有元素拷贝到扩容后到新数组中
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
// 遍历过程中,如果遇到陈旧的Entry,直接把value置为null,便于GC
if (k == null) {
e.value = null; // Help the GC
} else {
// 重新计算Entry在新数组的位置
int h = k.ThreadLocalHashCode & (newLen - 1);
// 如果冲突了,使用「开放地址法-线性探测」往后找
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
// 设置新哈希表的threshHold和size属性
setThreshold(newLen);
size = count;
table = newTab;
}
2、getEntry 方法
get()
方法就是 根据 ThreadLocal
计算出数组下标i
,然后返回 table[i]
的数据。
private Entry getEntry(ThreadLocal<?> key) {
// 根据key的hash值(非hashCode)计算出对应Entry的位置
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);
}
/**
* Version of getEntry method for use when key is not found in its direct hash slot.
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// e不为空 -> key计算出的位置可能会有冲突(比如预期位置是p=5,但是p=5的位置已经有其他Entry了)
while (e != null) {
ThreadLocal<?> k = e.get();
// Entity匹配(key的hash值相同),则返回相应的Entity
if (k == key)
return e;
if (k == null)
// 删除陈旧的Entity(key为null)
expungeStaleEntry(i);
else
// 使用「开放地址法-线性探测」往后找
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
3、remove 方法
remove()
方法就是 根据 ThreadLocal
计算出数组下标i
,然后移除 table[i]
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
// 根据key的hash值(非hashCode)计算出对应Entry的位置
int i = key.threadLocalHashCode & (len-1);
// 判断指定下标的entry是否存在,如果不存在,使用「开放地址法-线性探测」往后找
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
// 将entry的key置为null(Reference的clear方法)
e.clear();
// 删除陈旧的Entity(key为null)
expungeStaleEntry(i);
return;
}
}
}
public abstract class Reference<T> {
private T referent; /* Treated specially by GC */
public void clear() {
this.referent = null;
}
}
4、expungeStaleEntry 方法
expungeStaleEntry
方法用于删除指定槽位的Entry
- 删除指定槽位的Entry:value置为null,table[i] 置为 null
- 从删除的槽位往后,Rehash所有Entry,放到正确的位置上,直到遇到null
/**
* 删除指定槽位的Entry
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 删除指定槽位的Entry
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 从删除的槽位往后,Rehash所有Entry,放到正确的位置上,直到遇到null
Entry e;
int i;
for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
// 删除陈旧的Entry
e.value = null;
tab[i] = null;
size--;
} else {
// 重新计算Entry在新数组的位置
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
// 换位置
tab[i] = null;
// 如果冲突了,使用「开放地址法-线性探测」往后找
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
五、ThreadLocal 内存泄漏问题
1、相关概念
在分析之前,先了解一下相关的概念:
1)内存溢出 & 内存泄漏
-
内存溢出(memory overflow):要用,但是不够了,导致OOM。
-
内存泄漏(memory lack):不用了,但是没有释放,造成系统内存的浪费。
当内存泄露到达一定规模后,造成系统能申请的内存较少,甚至无法申请内存,最终会导致内存溢出。
2)强引用 & 弱引用
- 强引用:只要强引用还存在,垃圾收集器就永远不会回收被引用的对象。
- 弱引用:在系统GC时,只要发现弱引用,不管 JVM 的内存空间是否足够,都会回收只被弱引用关联的对象。
2、ThreadLocal 的 内存泄漏
先回顾一下ThreadLocal的内部关系图(其中连线5是弱引用,其他连线都是强引用)
假设在业务代码中已经使用完 ThreadLocal 了
-
ThreadLocal ref
被回收了,连线4断开 -
由于 Entry 的 key 只持有
ThreadLocal对象
的弱引用,而此时没有任何强引用指向ThreadLocal对象
,因此
ThreadLocal对象
就可以顺利被GC回收,连线5断开,此时Entry中的 key ref = null -
但是,在 没有手动删除Entry 以及 Thread依然在运行 的前提下:
强引用链
Thread ref -> Thread对象 -> ThreadLocalMap -> Entry -> value
始终存在这就导致 value不会被回收,而这块value永远也不会被访问到了,导致value发生了内存泄漏。
可以看到,虽然弱引用使得 ThreadLocal对象
可以被及时回收,但是 Entry 的 value 仍然存在内存泄漏的问题。
3、内存泄漏和弱引用有关?
有些人看到 ThreadLocal 使用了弱引用,就猜想内存泄漏问题是弱引用导致的,看完上面的例子,应该知道这是不对的。
我们假设 ThreadLocal 使用强引用(也就是关系图中的连线5是强引用)
假设在业务代码中已经使用完 ThreadLocal 了
-
ThreadLocal ref
被回收了 -
由于 Entry 的 key 对
ThreadLocal对象
是强引用,导致ThreadLocal对象
无法被回收。 -
在 没有手动删除Entry 以及 Thread依然在运行 的前提下:
强引用链
Thread ref -> Thread对象 -> ThreadLocalMap -> Entry
始终存在这就导致 Entry不会被回收,导致Entry发生了内存泄漏(此时Entry中包含 ThreadLocal对象 和 value)。
可见,ThreadLocal 的 内存泄漏和强引用完全没关系。
4、如何避免内存泄漏?
其实通过上面两个例子,可以发现内存泄漏的两个前提:
- 没有手动删除Entry
- Thread依然在运行
因此,避免内存泄漏也从这两点入手即可
-
使用完 ThreadLocal 后,调用 remove 方法删除对应的Entry,就能避免内存泄漏。
-
我们知道,ThreadLocalMap 是 Thread 的一个属性,
那么如果 Thread结束了,ThreadLocalMap自然也会被GC,从根源上避免了内存泄漏。
在实际的工作中,我们肯定会使用到线程池,而线程池的线程是复用的,所以上面第二点也就比较难达成了。
因此,我们在使用完 ThreadLocal 后,一定要及时调用 remove 方法,避免内存泄漏。
5、为什么要使用弱引用?
通过之前两个案例的分析,我们知道,无论使用强引用还是弱引用,都无法避免内存泄漏的问题,那么我们为什么要使用弱引用呢?难道就是为了能及时回收 ThreadLocal对象
吗?
看过之前分析源码的两章之后,应该可以发现:
- 在每次
get
、set
、remove
时,都会清理陈旧的Entry(也就是key为null的Entry)
这就意味着,在使用完 ThreadLocal 并且 Thread依然在运行 的前提下,就算忘了调用 remove 方法,弱引用也比强引用多一层保障:
- 发生内存泄漏的value,在下一次调用
get
、set
、remove
中的任意一个方法的时候都会被清除。
六、InheritableThreadLocal
在系统中,为了优化运行速度,会使用多线程编程,为了保证调用链ID能够在多线程间传递,需要考虑ThreadLocal
的传递问题。
InheritableThreadLocal
主要用于子线程创建时,自动继承父线程的ThreadLocal
变量,方便信息的进一步传递。
1、Thread 类的属性
Thread类中包含 threadLocals 和 inheritableThreadLocals 两个属性
public class Thread implements Runnable {
// 当前线程的ThreadLocalMap,主要存储该线程自身的ThreadLocal
ThreadLocal.ThreadLocalMap threadLocals = null;
// 自父线程集成而来的ThreadLocalMap,主要用于父子线程间ThreadLocal变量的传递
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
其中 inheritableThreadLocals
主要存储可向子线程中传递的 ThreadLocal.ThreadLocalMap
。
2、Thread 的 init
在构造 Thread 时,都会调用init
方法
public class Thread implements Runnable {
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
// 每个构造都会调用,这里省略...
}
private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
// 采用默认方式创建子线程时,inheritThreadLocals = true
init(g, target, name, stackSize, null, true);
}
// inheritThreadLocals = true
private void init(ThreadGroup g, Runnable target, String name, long stackSize,
AccessControlContext acc, boolean inheritThreadLocals) {
// ....
Thread parent = currentThread();
// ....
// 若此时父线程的inheritableThreadLocals不为空,则将父线程inheritableThreadLocals传递至子线程。
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
// ....
}
3、InheritableThreadLocal
InheritableThreadLocal
继承了ThreadLocal
,重写了以下3个方法:
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
/**
* 该函数在父线程创建子线程,向子线程复制InheritableThreadLocal变量时使用
*/
protected T childValue(T parentValue) {
return parentValue;
}
/**
* 由于重写了getMap,操作InheritableThreadLocal时,
* 将只影响Thread类中的inheritableThreadLocals变量,
* 与threadLocals变量不再有关系
*/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
/**
* 类似于getMap,操作InheritableThreadLocal时,
* 将只影响Thread类中的inheritableThreadLocals变量,
* 与threadLocals变量不再有关系
*/
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
4、createInheritedMap
Thread
的init
中,调用了createInheritedMap
方法,将父线程的inheritableThreadLocals
传递至子线程。
public class ThreadLocal<T> {
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
// 参数是父线程的inheritableThreadLocals
return new ThreadLocalMap(parentMap);
}
}
public class ThreadLocal<T> {
static class ThreadLocalMap {
/**
* 该构造只被 createInheritedMap() 调用.
*/
private ThreadLocalMap(ThreadLocalMap parentMap) {
// 父线程的 ThreadLocalMap 的 Entry数组
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
// 逐一复制 parentMap 的记录
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
// childValue内部就是直接将传入的value返回
// 至于这里为什么要使用childValue,而不是直接赋值,
// 个人认为是为了代码的可读性
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
}
}
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
// childValue方法就是将传参直接返回
protected T childValue(T parentValue) {
return parentValue;
}
}
从ThreadLocalMap
可知,子线程将parentMap
中的所有记录逐一复制至自身线程。
5、使用示例
public class InheritableThreadLocalDemo {
// 创建一个 InheritableThreadLocal 实例
private static final InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
// 在主线程中设置线程本地变量的值
threadLocal.set("Main Thread Value");
// 创建子线程并启动
Thread childThread = new Thread(() -> {
// 在子线程中获取父线程中线程本地变量的值
String value = threadLocal.get();
System.out.println("Child Thread Value: " + value);
});
childThread.start();
// 在主线程中等待子线程执行完毕
childThread.join();
// 在主线程中获取线程本地变量的值
String mainThreadValue = threadLocal.get();
System.out.println("Main Thread Value: " + mainThreadValue);
}
}
代码结果如下所示:
Child Thread Value: Main Thread Value
Main Thread Value: Main Thread Value
可以看到,子线程获取到了父线程本地变量的值。