ThreadLocal源码分析
简单分析
什么是ThreadLocal
ThreadLocal顾名思义可以理解为线程本地变量。也就是说如果定义了一个ThreadLocal,每个线程往这个ThreadLocal中读写是线程隔离(也就是每个线程读写都是自己的一份独立对象,与其他线程是无关的,当然前提是不同线程set的不是同一个对象的引用),互相之间不会影响的。它提供了一种将可变数据通过每个线程有自己的独立副本从而实现线程封闭的机制。
大致实现思路
Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。ThreadLocalMap有自己的独立实现,可以简单地将它的key视作为ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是他的一个人弱引用)。每个线程在往某个ThreadLocal里赛值得时候,都会往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
使用场景
1.如在一个AccountService中写了一段类似这样的代码:
Context ctx = new Context();
ctx.setTrackerID(.....)
然后这个AccountService 调用了其他Java类,不知道经过了多少层调用以后,最终来到了一个叫做AccountUtil的地方,在这个类中需要使用Context中的trackerID来做点儿事情:
很明显,这个AccountUtil没有办法拿到Context对象,怎么办?
1)可以在调用每个方法的时候,讲Context对象一层一层往下传递。
存在的问题:会修改很多类的代码,更重要的是有些类是第三方的根本没有可能去修改源代码。
2)讲Context中的set/get方法改成静态的,然后再AccountUtil中直接Context.get调用即可
public class Context{
public static String getTrackerID(){
......
}
public static void setTrackerID(String id){
......
}
}
这的确解决了一层一层传递的麻烦,但是出现了一个致命的问题。多线程并发问题!!!!!!!!!!
3)这个时候我们能不能把这个值放到线程中?让线程携带这个值,这样我们无论在任何地方都可以轻松获取,且是线程安全的。也就是每个线程都有一个私家领地。
public class Context {
private static final ThreadLocal<String> mThreadLocal
= new ThreadLocal<String>();
public static void setTrackerID(String id) {
mThreadLocal.set(id);
}
public static String getTrackerID() {
return mThreadLocal.get();
}
}
2.以session为列理解ThreadLocal
在web开发的session中,不同的线程对应不同的session,那么如何针对不同的线程获取对应的session呢?
1)在action中创建session,然后传递给Service,Service再传递给Dao,很明显,这种方式将代码变得臃肿复杂。
2)使用一个类,SesssionFacoty里面封装getSeesion的静态方法,然后在每个Dao中SesssionFacoty.getSession()。存在并发访问。
当然,对其方法加锁,这样的话效率上存在一定的问题。
3)创建一个静态的map,键对应我们的线程,值对应session,当我们想获取session时,只需要获取map,然后根据当前的线程就可以获取对应的值。
private static final ThreadLocal<Session> threadSession = new ThreadLocal();
public static Session getSession() throws InterruptedException {
Session s = threadSession.get();
try {
if (null == s){
s = getSessionFactory.openSession();
threadSession.set(s);
}
}catch (HibernateException e){
throw new InfrastructureExecption(e);
}
return s;
}
在Hibernate中通过使用ThreadLocal来实现的。在getSession方法中,如果ThreadLocal存在session,则返回session,否则创建一个session放入ThreadLocal中。
源码分析
ThreadLocalMap
存储结构
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
//自带一种基于弱引用的垃圾清理机制
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
//key为ThreadLocal
Entry(ThreadLocal<?> k, Object v) {
//ThreadLocal中存放的是ThreadLocal的弱引用
//这里虽然使用弱引用在key可以被回收,但是value还存在,所以不正确使用ThreadLocal可能会出现内存溢出
super(k);
value = v;
}
}
为什么需要使用弱引用?
因为如果这里使用普通的key-value形式来定义存储结果,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,那么GC就没办回收,而程序本身也无法判断是否可以清理节点。
弱引用是Java中四档引用中的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下次GC。当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利。
引用类型
1)强引用:就是指程序代码中普遍存在的,类似于“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象,也就是说即使Java虚拟机内存空间不足时,GC收集器也绝不会回收该对象,如果内存空间不够就会导致内存溢出。
2)软引用:用来描述一些还有用,但并非必需的对象。对于软引用关联着的对象,在系统要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行回收,以免出现内存溢出。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。在 JDK 1.2 之后,提供了 SoftReference 类来实现软引用。
2)弱引用:也就是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾回收器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在 JDK 1.2 之后,提供了 WeakReference 类来实现弱引用。ThreadLocal使用到的就有弱引用。
4)虚引用:也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。在 JDK 1.2 之后,提供了PhantomReference 类来实现虚引用。
ThreadLocal内存溢出
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。
但是这些被动的预防措施并不能保证不会内存泄漏。
1)使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏
2)分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就有可能会导致内存泄漏。
其实ThreadLocal是否会引起内存泄漏也是一个比较有争议性的问题。
认为ThreadLocal会引起内存泄漏的说法是因为如果一个ThreadLocal对象被回收了,我们往里面放的value对于【当前线程->当前线程的threadLocals(ThreadLocal.ThreadLocalMap对象)->Entry数组->某个entry.value】这样一条强引用链是可达的,因此value不会被回收。
认为ThreadLocal不会引起内存泄漏的说法是因为ThreadLocal.ThreadLocalMap源码实现中自带一套自我清理的机制。
之所以有关于内存泄露的讨论是因为在有线程复用如线程池的场景中,一个线程的寿命很长,大对象长期不被回收影响系统运行效率与安全。如果线程不会复用,用完即销毁了也不会有ThreadLocal引发内存泄露的问题。《Effective Java》一书中的第6条对这种内存泄露称为unintentional object retention(无意识的对象保留)。
当我们仔细读过ThreadLocalMap的源码,我们可以推断,如果在使用的ThreadLocal的过程中,显式地进行remove是个很好的编码习惯,这样是不会引起内存泄漏。
那么如果没有显式地进行remove呢?只能说如果对应线程之后调用ThreadLocal的get和set方法都有很高的概率会顺便清理掉无效对象,断开value强引用,从而大对象被收集器回收。
但无论如何,我们应该考虑到何时调用ThreadLocal的remove方法。一个比较熟悉的场景就是对于一个请求一个线程的server如tomcat,在代码中对web api作一个切面,存放一些如用户名等用户信息,在连接点方法结束后,再显式调用remove。
类成员变量与相应方法
/**
* The initial capacity -- MUST be a power of two.
* 初始容量,必须为2的幂
*/
private static final int INITIAL_CAPACITY = 16;
/**
* Entry表,大小为2的幂
* 对于2的幂作为模数取模,可以用&(2^n-1)来替代%2^n,位运算比取模效率高很多。
* 至于为什么,因为对2^n取模,只要不是低n位对结果的贡献显然都是0,会影响结果的只能是低n位。
*/
private Entry[] table;
/**
* 表里entry的个数
*/
private int size = 0;
/**
* 重新分配表大小的阙值,默认为0
*/
private int threshold; // Default to 0
//色湖之resize阙值以维持最坏的2/3的装载因子
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/**
* 使得Entry形成一个环形。所以它这里并不是像HashMap中,是一个数组,然后每个数组中下面挂上一个链路(java8是二叉树),
* 在这里它使用的是线性探测法,即如果hash索引到某个index,产生冲突会往下遍历寻找下一个。
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
/**
* 构造一个包含firstKey和firstValue的map
* ThreadLocalMap是惰性构造的,所以只有当至少要往里面放一个元素的时候才会构建它。
*/
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);
}
getEntry方法
这个方法会被ThreadLocal的get方法直接调用,用于获取map中某个ThreadLocal存放的值。
private Entry getEntry(ThreadLocal<?> key) {
// 根据key这个ThreadLocal的ID来获取索引,也即哈希值
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 对应的entry存在且未失效且弱引用指向的ThreadLocal就是key,则命中返回
if (e != null && e.get() == key) {
return e;
} else {
// 因为用的是线性探测,所以往后找还是有可能能够找到目标Entry的。
return getEntryAfterMiss(key, i, e);
}
}
/*
* 调用getEntry未直接命中的时候调用此方法
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 基于线性探测法(一个一个往下找)不断向后探测直到遇到空entry。
while (e != null) {
ThreadLocal<?> k = e.get();
// 找到目标
if (k == key) {
return e;
}
if (k == null) {
// 该entry对应的ThreadLocal已经被回收,调用expungeStaleEntry来清理无效的entry
expungeStaleEntry(i);
} else {
// 环形意义下往后面走,
i = nextIndex(i, len);
}
e = tab[i];
}
return null;
}
/**
* 这个函数是ThreadLocal中核心清理函数,它做的事情很简单:
* 就是从staleSlot开始遍历,将无效(弱引用指向对象被回收)清理,即对应entry中的value置为null,将指向这个entry的table[i]置为null,直到扫到空entry。
* 另外,在过程中还会对非空的entry作rehash。
* 可以说这个函数的作用就是从staleSlot开始清理连续段中的slot(断开强引用,rehash slot等)
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 因为entry对应的ThreadLocal已经被回收,value设为null,显式断开强引用
tab[staleSlot].value = null;
// 显式设置该entry为null,以便垃圾回收
tab[staleSlot] = null;
size--;
Entry e;
int i;
for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 清理对应ThreadLocal已经被回收的entry
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
/*
* 对于还没有被回收的情况,需要做一次rehash。
* 如果对应的ThreadLocal的ID对len取模出来的索引h不为当前位置i,
* 则从h向后线性探测到第一个空的slot,把当前的entry给挪过去。
*/
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null) {
h = nextIndex(h, len);
}
tab[h] = e;
}
}
}
// 返回staleSlot之后第一个空的slot索引
return i;
}
总结:
1)根据入参threadLocal的threadLocalHashCode对表容量取模得到index
2)如果index对应的solt就是要读的threadLoacl,则直接返回结果
3)调用getEntryAfterMiss线性探测,过程中每碰到无效slot,调用expungeStaleEntry进行段清理(清理只是再get没有得到的情况下才有可能发生);如果找到了key,则返回结果entry
4)没有找到key,返回null
set方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len - 1);
// 线性探测
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 找到对应的entry
if (k == key) {
e.value = value;
return;
}
// 替换失效的entry
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold) {
rehash();
}
}
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// 向前扫描,查找最前的一个无效slot
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len)) {
if (e.get() == null) {
slotToExpunge = i;
}
}
// 向后遍历table
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 找到了key,将其与无效的slot交换
if (k == key) {
// 更新对应slot的value值
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
/*
* 如果在整个扫描过程中(包括函数一开始的向前扫描与i之前的向后扫描)
* 找到了之前的无效slot则以那个位置作为清理的起点,
* 否则则以当前的i作为清理起点
*/
if (slotToExpunge == staleSlot) {
slotToExpunge = i;
}
// 从slotToExpunge开始做一次连续段的清理,再做一次启发式清理
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 如果当前的slot已经无效,并且向前扫描过程中没有无效slot,则更新slotToExpunge为当前位置
if (k == null && slotToExpunge == staleSlot) {
slotToExpunge = i;
}
}
// 如果key在table中不存在,则在原地放一个即可
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 在探测过程中如果发现任何无效slot,则做一次清理(连续段清理+启发式清理)
if (slotToExpunge != staleSlot) {
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
}
/**
* 启发式地清理slot,
* i对应entry是非无效(指向的ThreadLocal没被回收,或者entry本身为空)
* n是用于控制控制扫描次数的
* 正常情况下如果log n次扫描没有发现无效slot,函数就结束了
* 但是如果发现了无效的slot,将n置为table的长度len,做一次连续段的清理
* 再从下一个空的slot开始继续扫描
*
* 这个函数有两处地方会被调用,一处是插入的时候可能会被调用,另外个是在替换无效slot的时候可能会被调用,
* 区别是前者传入的n为元素个数,后者为table的容量
*/
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
// i在任何情况下自己都不会是一个无效slot,所以从下一个开始判断
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
// 扩大扫描控制因子
n = len;
removed = true;
// 清理一个连续段
i = expungeStaleEntry(i);
}
} while ((n >>>= 1) != 0);
return removed;
}
private void rehash() {
// 做一次全量清理
expungeStaleEntries();
/*
* 因为做了一次清理,所以size很可能会变小。
* ThreadLocalMap这里的实现是调低阈值来判断是否需要扩容,
* threshold默认为len*2/3,所以这里的threshold - threshold / 4相当于len/2
*/
if (size >= threshold - threshold / 4) {
resize();
}
}
/*
* 做一次全量清理
*/
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) {
/*
* 个人觉得这里可以取返回值,如果大于j的话取了用,这样也是可行的。
* 因为expungeStaleEntry执行过程中是把连续段内所有无效slot都清理了一遍了。
*/
expungeStaleEntry(j);
}
}
}
/**
* 扩容,因为需要保证table的容量len为2的幂,所以扩容即扩大2倍
*/
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 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();
if (k == null) {
e.value = null;
} else {
// 线性探测来存放Entry
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null) {
h = nextIndex(h, newLen);
}
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
总结:
1)探测过程中slot都不无效,并且顺利找到key所在的slot,直接替换即可
2)在探测过程中发现无效solt,调用replaceStableEntry,效果是最终一定会把key和value放在这个slot,并且会尽可能清理无效slot
- 在replaceStaleEntry过程中,如果找到了key,则做一个swap把它放到那个无效slot中,value置为新值
- 在replaceStaleEntry过程中,没有找到key,直接在无效slot原地放entry
3)探测没有发现key,则在连续段末尾的后一个空位置放上entry,这也是线性探测法的一部分。放完后,做一次启发式清理,如果没清理出去key,并且当前table大小已经超过阈值了,则做一次rehash,rehash函数会调用一次全量清理slot方法也即expungeStaleEntries,如果完了之后table大小超过了threshold - threshold / 4,则进行扩容2倍
remove方法
/**
* 从map中删除ThreadLocal
*/
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len - 1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
// 显式断开弱引用
e.clear();
// 进行段清理
expungeStaleEntry(i);
return;
}
}
}
ThreadLocal
Thread、ThreadLocal,ThreadLocalMap关系
一个Thread中只有一个ThreadLocalMap(Thread中有一个成员变量ThreadLocal.ThreadLocalMap threadLocals = null;),一个ThreadLocalMap中可以有多个ThreadLoacl对象,其中一个ThreadLocal对象对应一个ThreadLocalMap中一个的Entry实体。
ThreadLoacl源码分析
public void set(T value) {
//获取调用改set方法的线程
Thread t = Thread.currentThread();
//根据线程去获取ThreadLocalMap,如果还为初始化,那么就调用createMap创建初始化,构造直接map.set(this,value)即可
ThreadLocalMap map = getMap(t);
if (map != null)
//直接调用ThreadLocalMap的set方法
map.set(this, value);
else
//线程第一次set,初始化ThreadLocalMap
createMap(t, value);
}
//初始化Tread中的ThreadLocal.ThreadLocalMap threadLocals
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
public T get() {
Thread t = Thread.currentThread();
//获取该线程的ThreadLoalMap对象
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;
}
}
//如果ThreadLocalMap还未被初始化,就返回默认值
return setInitialValue();
}
//Removes the current thread's value for this thread-local variable.
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
参考博客
一个故事讲明白ThreadLocal:https://mp.weixin.qq.com/s/aM03vvSpDpvwOdaJ8u3Zgw
ThreadLocal源码解读:https://www.cnblogs.com/micrari/p/6790229.html
Java多线程编程-(18)-借ThreadLocal出现OOM内存溢出问题再谈弱引用WeakReference:https://blog.csdn.net/xlgen157387/article/details/78513735?ref=myread