一文详细解释JDK中的ThreadLocal,通俗易懂

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开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
img

最后希望可以帮助到大家!

千千万万要记得:多刷题!!多刷题!!

之前算法是我的硬伤,后面硬啃了好长一段时间才补回来,算法才是程序员的灵魂!!!!

篇幅有限,以下只能截图分享部分的资源!!

(1)多线程(这里以多线程为代表,其实整理了一本JAVA核心架构笔记集)

image

(2)刷的算法题(还有左神的算法笔记)

image

(3)面经+真题解析+对应的相关笔记(很全面)

image

(4)视频学习(部分)

ps:当你觉得学不进或者累了的时候,视频是个不错的选择

在这里,最后只一句话:祝大家offer拿到手软!!

点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频**

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
[外链图片转存中…(img-6UFPWRxT-1710747152601)]

最后希望可以帮助到大家!

千千万万要记得:多刷题!!多刷题!!

之前算法是我的硬伤,后面硬啃了好长一段时间才补回来,算法才是程序员的灵魂!!!!

篇幅有限,以下只能截图分享部分的资源!!

(1)多线程(这里以多线程为代表,其实整理了一本JAVA核心架构笔记集)

[外链图片转存中…(img-S6i8FDRD-1710747152602)]

(2)刷的算法题(还有左神的算法笔记)

[外链图片转存中…(img-i5vg0ebw-1710747152602)]

(3)面经+真题解析+对应的相关笔记(很全面)

[外链图片转存中…(img-XGCnahk6-1710747152602)]

(4)视频学习(部分)

ps:当你觉得学不进或者累了的时候,视频是个不错的选择

在这里,最后只一句话:祝大家offer拿到手软!!

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 17
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值