java threadlocal ThreadLocal源码分析图文版
java threadlocal ThreadLocal源码分析图文版
引言
正如文章标题,本文重点在于剖析的源码,先对下定义
是线程级别的私有变量
即使你没有使用过也可以阅读,本文会从最基本的使用入手,结合源码及图片由浅入深地分享我在学习源码中的收获和理解,希望对你有帮助.
一、初识.1 的使用
ThreadLocal traceId = new ThreadLocal()
//ThreadLocal traceId = ThreadLocal.withInitial(()->"initial");
...
traceId.set(UUID.randomUUID().toString);
...
traceId.get();
...
traceId.remove()
这是一个精简后的传统使用场景,在过去的web应用里,请求到达服务器以后可以分配一个用于追踪请求到响应的链路,或者存储单个请求内的用户身份信息等等.尽管最终执行的时候分发到了不同的线程内java threadlocal ThreadLocal源码分析图文版,每个线程内可见的数据依旧是自己的用户身份信息或者,在当前线程内的所有地方都可以通过实例.get()的方式获取到这个信息.得益于的线程隔离性,无需担心当前线程会取到其他线程的结果.
之所以说是在过去的web应用,是因为在当下一条请求到达服务器以后几乎一定会经历异步执行或途经多个微服务,这时作为线程隔离的变量就不够用了,无法把一条链路中所需的信息传递下去.但这并不代表就无用武之地了,恰恰相反,JDK原生的al(简称ITL,先眼熟,后文还会见到),用于跨线程传递的组件MDC和阿里开源的ocal(简称TTL)都是基于实现的,这些仅仅是冰山一角.至于如何基于做到上述的扩展,我之后也会抽空更新对于MDC和TTL的分享.
我在注释中补充了除了最常用的set(),get()和()以外,提供的方法,它允许通过表达式的方式给一个赋予初始值.
1.2 剥开的外壳
对外暴露的方法实际上就只有这么多java threadlocal ThreadLocal源码分析图文版,你可以直接把当成一个线程隔离的容器,通过set()和get()进行存取.但如果在工作和面试中有人问你为什么具备线程隔离性,那只会使用就不够了
首先我们来看内部的两个私有变量
ThreadLocal.ThreadLocalMap threadLocals = null;
//1.1中提到的ITL
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
也就是说每一个线程内部实际上都持有一个,它是的静态内部类,存放着当前线程内的所有实例.我们再继续往下看内部的结构
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;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0;
private int threshold;
本质上是一个用Entry数组实现的简易哈希表,其中每一个Entry继承了弱引用,Entry的key存放,value存放值;
除此之外,我还节选了一部分参数,比如这个哈希表的初始容量是16,初始大小是0,用于控制哈希表扩容的阈值等等.提到哈希表的实现,就一定绕不开如何处理哈希碰撞等细节,但我认为目前为止对于学习是如何实现线程隔离的已经足够了java threadlocal,感兴趣的话我会单独出一篇文章介绍内部是如何实现的.
结合这张图,我们再以.set() 和 get()来回看中提供的方法内部是如何实现的:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//空实现,返回null
return setInitialValue();
}

可以看到,不管是set()还是get(),都会首先获取当前线程,并拿到当前线程下的,再调用map内部的set()和get()方法,我们再来看内部的set()和get()是如何实现的
//set核心代码
private void set(ThreadLocal key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
...
e.value = value;
return;
}
//get核心代码
private Entry getEntry(ThreadLocal key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
return e;
}
可以看到,在set()和get()的时候都会根据实例内部的和哈希掩码做与运算来计算哈希槽的下标,直接修改或返回中的value即可.至此为止,的面纱就已经被彻底揭开:只是暴露给使用者的实例,自身并没有存储值,而是在内部某一个Entry的value里,找到这个value的方式就是通过计算得到哈希槽下标.
补充一句java threadlocal,在set()方法中我们可以看到实际上采用了延迟加载,实例没有赋值时指向的是null,只有当有set()操作发生时才会初始化,代码如下
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
我之所以在set()和get()的代码块中注释了核心代码,就是因为还隐藏了许多内部的细节,譬如如果发生了哈希碰撞怎么处理,以及和标记和清除过期哈希槽相关的逻辑二、使用时的注意事项2.1 典中典之内存泄漏
这个问题在面试过程中可以说是必背的八股,但未必每个人都真正理解了背后的含义,这和JVM运行时的内存结构,引用级别和GC息息相关.通过上一章的内容你已经可以对如何实现线程隔离侃侃而谈,那么这一小节旨在帮你真正理解为什么会产生内存泄露的问题java threadlocal,以及如何在使用过程中避免内存泄漏.
废话不多说,直接上图:
如果你对这张图中出现的JVM内存结构或引用级别还不够了解,就无法真正理解老生常谈的内存泄漏问题,这部分网上的资料非常多也很全面2.1.1 为什么Entry的key采用弱引用
前面我们说过自身并不存储值,它是暴露给使用者的,本质上还是在操作.不妨想想如果某个的实例在程序中被设为null,Entry的key作为强引用时会发生什么,这显然会导致内存泄漏,这是因为:
对使用者而言指向堆上对象的引用就只有自己的实例,但通过前面的学习我们知道实际上还有一份引用存在于Entry的key里,所以key只能是弱引用,否则在堆上的对象实例就永远不会被回收.
因此,每次GC的时候都会回收堆上只有弱引用指向的对象
2.1.2 怎么还有内存泄漏?
Entry的key通过设置为弱引用解决了,但value显然行不通,它只能是强引用.所以面试八股中老生常谈的内存泄漏问题指的是当key被回收设置为null以后,value的强引用依然存在.
为了应对这种情况,的get(),set()和()方法中均提供了对中key为null时删除对应value的操作.
不同之处在于:
因此,最佳实践就是不再使用实例时显示调用()
2.2 数据污染
线程池内部如果使用了要格外注意,因为线程池内线程会被复用,一定要及时(),避免造成数据污染
2.3 线程隔离等于线程安全吗?

直接说结论,不等于.当中的value是引用类型时,比如MDC中的Map.这种情况下要么明确使用场景确保没有通过本地引用修改value,要么返回引用时采用深拷贝.
三、浅析al
在引言中提到了线程级别的已经不能满足当下跨线程的需要,ITL就是JDK原生的支持父子线程间携带信息的扩展,代码非常简短:
public class InheritableThreadLocal extends ThreadLocal {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
直接继承自并重写了三个方法,第一个方法决定了父子线程传递值的方式是直接传递,后两个方法仅仅把创建和获取的实例设置为ITL独立的map实例.如果你回看1.2中的代码,你会发现ITL已经声明了自己的.
那怎么使用呢?实际上你只需要声明一个ITL,然后像一样使用就可以了,父子线程的传递是自动完成的,怎么做到的?
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
这段代码来自于内的构造方法,可以看到只要当前线程的布尔值为真且父线程的ITL是有值的,子线程会接受()的结果,这个方法的内部就是对父线程的ITL做了深拷贝并返回.那问题又来了,大多都是直接new ()再重写里面的run()方法的,我怎么知道这个布尔值是多少?只要再往上追一层对外共开的无参构造方法就水落石出了:
public Thread(Runnable target) {
this(null, target, "Thread-" + nextThreadNum(), 0);
}
public Thread(ThreadGroup group, Runnable target, String name,
long stackSize) {
this(group, target, name, stackSize, null, true);
}
可以看到,这个布尔值是默认为true的,这也是为什么前面说父子线程的传递是自动完成的.
实际上ITL对做的扩展依旧不够,当下绝大部分都是采用线程池复用的方式执行异步任务,而不是简单的父子线程.这就需要类似MDC和TTL这样的技术了,值得一提的是,它们的实现方式依然是对做封装.其中MDC的源码实际上非常简短,而阿里的TTL拥有非常友好和活跃的社区氛围,推荐直接在官方文档处学习:/–local: a Java std lib( & 0-) for /, an al that even using . ()四、总结
本文结合了部分的源码 以及 静态代码和JVM运行时的图示,主要讲解了以下几点:
1.如何做到线程隔离
每个下持有一个,以弱引用key的方式存储在中,真正的值存储在value.
2.使用时的注意事项
3.ITL如何做到父子线程传递
在new ()内部自动拷贝父线程的ITL
五、todo
对于源码的解析中隐藏的内部对简易哈希表的实现,以及没有展开讲的MDC和TTL这样的跨线程传递方案,后续会抽时间梳理并分享.
水平有限,文章难免存在谬误和不完善的地方,随时欢迎批评指正,补充或者分享自己对于设计的看法.
1. 本站所有资源来源于用户上传和网络,如有侵权请联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系站长处理!
6. 本站不售卖代码,资源标价只是站长收集整理的辛苦费!如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。
7. 站长QQ号码 2205675299
资源库 - 资源分享下载网 » java threadlocal ThreadLocal源码分析图文版
常见问题FAQ
- 关于资源售价和售后服务的说明?
- 代码有没有售后服务和技术支持?
- 有没有搭建服务?
- 链接地址失效了怎么办?
- 关于解压密码

