[Interview系列 知识储备回顾] 线程篇[2]
ThreadLocal
上下文是贯穿整个系统或者阶段生命周期的对象, 包含了系统全局的一些信息.
自从JDK1.2
起, Java就提供了java.lang.ThreadLocal
为每一个使用该变量的线程都提供了独立的副本, 可以做到线程间的数据隔离, 每一个线程都可以访问各自内部的副本变量.
ThreadLocal的使用场景及注意事项
- 在进行对象跨层传递的时候, 可以考虑
ThreadLocal
, 避免方法多次传递, 打破层次间的约束.(MethodA->MethodB->MethodC->MethodD) - 线程间数据隔离
- 进行事务操作, 用于存储线程间的事务信息
ThreadLocal
并不是解决多线程共享只有的技术, 一般情况下, 每一个线程的ThreadLocal
存储的都是一个新的对象, 如果ThreadLocal
存储的是一个对象的引用, 还是会出现数据不一致等并发问题.
ThreadLocal 源码详解
由官方javadoc给出exmaple, 可以了解到其两个API(initialValuie和get)
initialValue
方法为ThreadLocal
要保存的数据类型指定了一个初始化值.
get
方法用于返回当前线程在ThreadLocal
中的数据备份, 当前线程的数据都存放在一个ThreadLocalMap
的数据结构中.get
具体流程
- 获取当前线程
- 根据当前线程获取
ThreadLocalMap
, 这里每个Thread类中都有个属性是跟ThreadLocalMap
关联的. - 如果map不为null, 则以当前的
ThreadLocal
为key获取对应的Entry
- 如果
Entry
不为null, 则直接返回Entry的value值, 反之进入(5)
- 如果在
(2)
获取不到对应的ThreadLocalMap
, 需要执行setInitialValue
方法 - 在
setInitialValue
方法中先通过initialValue
方法获取初始值 - 根据当前线程Thread获取对应的ThreadLocalMap
- 如果
ThreadLocalMap
不为null, 则为map指定initialValue
所获取的初始值.实际上在map.set(this, value)方法中new了一个Entry对象 - 如果
ThreadLocalMap
为null, 则创建一个ThreadLocalMap
, 并且与Thread
对象的threadLocals
属性相关联. - 返回
initialValue
方法的结果
set
方法主要是为ThreadLocal
指定将要存储的数据.
其实跟get方法中的setinitialValue
是一样的. 只是少了获取initialValue
并返回这个操作而已.过程就不赘述了.
ThreadLocalMap
无论是get方法还是set方法都不可避免地要和
ThreadLocalMap
和Entry
打交道.在ThreadLocalMap
中用于存储数据的是Entry
, 它是一个WeakReference
类型的子类, 之所以设计为WeakReference
是为了能够在JVM发生垃圾回收事件时, 能够自动回收防止内存溢出的情况出现.
ThreadLocal 内存泄漏问题分析
对于内存泄漏我们都不陌生.
举个例子, 在某个线程结束了生命周期后,Thread
的实例和所存储的数据还存在contexts
中, 随着运行时间的增加, 在contexts
中会残留很多thread实例和数据.
-
ThreadLocalMap
#Entry设计为WeakReference
类型, 是保证在JVM中触发GC(young gc
、full gc
)时都会导致Entry
的回收. - 在get、set数据时增加检查, 清除已经被垃圾回收的
Entry
.
以下API会检查(key为null的Entry进行清除)
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;
if (k == null)
// 将key为null的Entry删除
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
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];
if (e != null && e.get() == null) {
n = len;
removed = true;
// 将key为null的Entry删除
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
// 执行Entry在ThreadLocalMap中的删除操作
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter 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) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
因此, ThreadLocal可以在一定程序上保证不发生内存泄漏.
借用<Java高并发详解 - 汪文君老师>书中一个例子.
ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();
TimeUnit.SECONDS.sleep(30);
threadLocal.set(new byte[1024 * 1024 * 100]); //100MB
threadLocal.set(new byte[1024 * 1024 * 100]); //100MB
threadLocal.set(new byte[1024 * 1024 * 100]); //100MB
threadLocal = null;
currentThread().join();
程序运行时, 借用VisualVM工具对JVM进程进行监控.
最后的100MB堆内存一直没有得到释放.
当threadLocal
显式指定为null
之后, 执行gc操作
, 堆内存中的threadLocal
被回收, 同时ThreadLocalMap
中的Entry.key
也变为null, 但是value
不会被释放. 除非当前线程结束了生命周期, thread
引用被回收.
内存泄漏
和内存溢出
是有区别的,内存泄漏
是导致内存溢出
的原因之一, 内存泄漏更多的是程序中不再持有某个对象的引用, 导致该对象无法被垃圾回收, 原因是因为该对象到根Root的链路是可达的
, 比如ThreadRef
到Entry.value
的引用链路.
上一篇: 搞笑也阅兵,这是逗B方阵
下一篇: 爆笑的小偷、小贩、懒汉和起名专家