2.3 ThreadLocalMap类
从上面的 ThreadLocal#get、#set、#remove 方法分析可以看到,最终这些操作都是在 ThreadLocalMap 上完成。文中最开始已介绍过 ThreadLocalMap 实际是通过 开放地址法 实现的,其内部的 Entry 数据组table用于存储 ThreadLocal 与保存在 ThreadLocal 的值,最终实现 ThreadLocal 内保存的值与线程绑定。
static class ThreadLocalMap {
// map的初始容量
private static final int INITIAL_CAPACITY = 16;
// Entry数组
private Entry[] table;
// Entry元素个数
private int size = 0;
// 阈值,用于扩容降低开放寻址时的冲突
private int threshold;
}
//ThreadLocalMap中的Entry持有ThreadLocal的弱引用。
static class Entry extends WeakReference<ThreadLocal<?>> {
// ThreadLocal上关联的值
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap构造函数
//创建ThreadLocalMap并在其上绑定第一个线程局部变量
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
//取模获取引用ThreadLocal的Entry的下标
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
//设置扩容的阈值
setThreshold(INITIAL_CAPACITY);
}
ThreadLocalMap#getEntry方法以 ThreadLocal 作为 Key 从 ThreadLocalMap 采用 开放寻址法 从 Entry 数组中寻找引用 ThreadLocal 对应的 Entry 。ThreadLocal#get 方法会调用该方法,获取保存在Entry内的Value,即 实际与当前线程绑定的变量值 。
private Entry getEntry(ThreadLocal<?> key) {
//取模试探性获取引用ThreadLocal的Entry的下标
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
//发生冲突,采用开放寻址法从Entry数组中找引用ThreadLocal的Entry
return getEntryAfterMiss(key, i, e);
}
//发生冲突,采用开放寻址法从Entry数组中找引用ThreadLocal的Entry
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key) // 找到引用ThreadLocal的Entry返回
return e;
if (k == null) //找到之前引用ThreadLocal的Entry,但ThreadLocal的引用已被remove方法清理掉或被GC清理掉
//通过重新哈希,清理已被remove或被GC回收的ThreadLocal上关联的value
expungeStaleEntry(i);
else // 继续向前寻找引用ThreadLocal的Entry
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
//开放寻址,获取元素下标
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
ThreadLocalMap#set方法以 ThreadLocal 作为 Key 采用开放寻址法将value与其所属线程绑定。
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//取模试探性获取引用ThreadLocal的Entry的下标
int i = key.threadLocalHashCode & (len-1);
//尝试用开放寻址的方法在Entry数组中找到之前引用ThreadLocal的Entry
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//找到之前引用ThreadLocal的Entry,重置value值并返回
if (k == key) {
e.value = value;
return;
}
//找到之前引用ThreadLocal的Entry,但ThreadLocal的引用已被remove方法清理掉或被GC清理掉
if (k == null) {
//重新将引用ThreadLocal的Entry放入到Entry数组中并清理已被remove或被GC回收的ThreadLocal上关联的value
replaceStaleEntry(key, value, i);
return;
}
}
//未找到之前引用ThreadLocal的Entry,创建Entry并放入Entry数组
tab[i] = new Entry(key, value);
int sz = ++size;
//清理槽位失败且Entry数组长度超过阈值,重新rehash对Entry数组扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
ThreadLocalMap#remove方法以 ThreadLocal 作为 Key 采用开放寻址法将value与其所属线程绑定。
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
//取模试探性获取引用ThreadLocal的Entry的下标
int i = key.threadLocalHashCode & (len-1);
//用开放寻址法找到引用ThreadLocal的Entry,并将其清除
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
//清除Entry上的弱引用ThreadLocal
e.clear();
//通过重新哈希,清理已被remove或被GC回收的ThreadLocal上关联的value
expungeStaleEntry(i);
return;
}
}
}
ThreadLocalMap#getEntry、#set、#remove方法内部最终都会尝试调用 expungeStaleEntry 方法。
expungeStaleEntry通过重新哈希,清理已被remove或被 GC 回收的 ThreadLocal 上关联的value, 该方法可以保证由于只与Entry存在弱引用关系的 ThreadLocal 被 GC 回收后,Entry上的Value(与 ThreadLocal 上关联的value)能被及时清理,而 不会因为Entry上的Value一直存在强引用最终导致的内存泄漏 。实际 ThreadLocal#set、#get、#remove 方法最终都会调用 expungeStaleEntry 方法。
// 通过重新哈希,清理已被remove或被GC回收的ThreadLocal上关联的value
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 清理当前位置上已被remove或被GC回收的ThreadLocal上关联的value
tab[staleSlot].value = null;
// 清理当前位置上的Entry
tab[staleSlot] = null;
size–;
// 向后重新哈希,直到对应位置上没有Entry。
Entry e;
int i;
for (i = nextIndex(staleSlot, len); (e = tab[i]) != null;i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//清理当前位置上已被remove或被GC回收的ThreadLocal上关联的value
if (k == null) {
e.value = null;
tab[i] = null;
size–;
} else {
//用开放寻址法,重新调整当前Entry在数组中的位置
int h = k.threadLocalHashCode & (len - 1);
//ThreadLocal初始位置h与i不一致,尝试将其放回始位置或开放寻址法后的位置
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
3 使用场景
本文开篇已介绍过, ThreadLocal 适用方法调用链上参数的透传,但要注意是同线程间,但不适合异步方法调用的场景。对于异步方法调用,想做参数的透传可以采用阿里开源的 Tran ittableThreadLocal**。权限、日志、事务等框架都可以利用 ThreadLocal 透传重要参数。
在使用 Spring Security 时,当用户认证通过后,业务逻辑处理中经常会去获取用户认证时的用户信息,通过会将这个功能封装在工具类中,如下的 SecurityUtils#getAuthUser 方法用于获取用户的认证信息,如果用户认证过返回用户信息,否则返回null。业务逻辑中直接通过 SecurityUtils#getAuthUser方法便能方便的获取用户的认证信息。
public class SecurityUtils {
public static User getAuthUser() {
try {
// 通过Srping Security 上下文获取用户的认证信息
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return Objects.nonNull(auth) ? (User) auth.getPrincipal() : null;
} catch (Exception e) {
log.error(“Get user auth info fail”, e);
throw new CustomException(“获取用户信息异常”, HttpStatus.UNAUTHORIZED.value());
}
}
}
SecurityContextHolder采用策略模式实现,实默认策略便是通过ThreadLocal存储Spring Security的上下文信息,这个上下文信息中包括认证信息。 ThreadLocalSecurityContextHolderStrategy 源码如下。
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
//采用ThreadLocal存储线程上下文信息
private static final ThreadLocal contextHolder = new ThreadLocal<>();
public void clearContext() {
contextHolder.remove();
}
public SecurityContext getContext() {
//从ThreadLocal获取线程上下文信息
SecurityContext ctx = contextHolder.get();
if (ctx == null) {
ctx = createEmptyContext();
contextHolder.set(ctx);
}
return ctx;
}
public void setContext(SecurityContext context) {
Assert.notNull(context, “Only non-null SecurityContext instances are permitted”);
contextHolder.set(context);
}
…
}
4 内存泄漏问题分析
4.1 内存漏泄示例
下面通过代码模拟ThreadLocal内存漏泄,注意运行指定的VM参数 -Xms大于50MB。
public class ThreadLocalOOM {
public static void main(String[] args) throws InterruptedException {
ThreadLocal tl = new CustomThreadLocal();
tl.set(new Value50MB());
//清理 CustomThreadLocal 对象的强引用
tl = null;
System.out.println(“Call System.gc method to trigger Full GC”);
System.gc();
//GC线程优先级较低,休眠3秒确保Full GC已完成
Thread.sleep(3000);
}
public static class CustomThreadLocal extends ThreadLocal {
private byte[] a = new byte[1024 * 1024 * 1];
@Override
public void finalize() {
// Full GC 如果对象被回收,该方法会被调用
System.out.println(“CustomThreadLocal 1 MB finalized.”);
小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
最后希望可以帮助到大家!
千千万万要记得:多刷题!!多刷题!!
之前算法是我的硬伤,后面硬啃了好长一段时间才补回来,算法才是程序员的灵魂!!!!
篇幅有限,以下只能截图分享部分的资源!!
(1)多线程(这里以多线程为代表,其实整理了一本JAVA核心架构笔记集)
(2)刷的算法题(还有左神的算法笔记)
(3)面经+真题解析+对应的相关笔记(很全面)
(4)视频学习(部分)
ps:当你觉得学不进或者累了的时候,视频是个不错的选择
在这里,最后只一句话:祝大家offer拿到手软!!
点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频**
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
[外链图片转存中…(img-6UFPWRxT-1710747152601)]
最后希望可以帮助到大家!
千千万万要记得:多刷题!!多刷题!!
之前算法是我的硬伤,后面硬啃了好长一段时间才补回来,算法才是程序员的灵魂!!!!
篇幅有限,以下只能截图分享部分的资源!!
(1)多线程(这里以多线程为代表,其实整理了一本JAVA核心架构笔记集)
[外链图片转存中…(img-S6i8FDRD-1710747152602)]
(2)刷的算法题(还有左神的算法笔记)
[外链图片转存中…(img-i5vg0ebw-1710747152602)]
(3)面经+真题解析+对应的相关笔记(很全面)
[外链图片转存中…(img-XGCnahk6-1710747152602)]
(4)视频学习(部分)
ps:当你觉得学不进或者累了的时候,视频是个不错的选择
在这里,最后只一句话:祝大家offer拿到手软!!