1. 背景
ThreadLocal源码解读,网上面早已经泛滥了,大多比较浅,甚至有的连基本原理都说的很有问题,包括百度搜索出来的第一篇高访问量博文,说ThreadLocal内部有个map,键为线程对象,太误导人了。
ThreadLocal非常适合对Java多线程编程感兴趣的程序员作为入门类阅读,原因两方面:
-
加上注释源码也不过七八百行。
-
结构清晰,代码简洁。
本文重点导读ThreadLocal中的嵌套内部类ThreadLocalMap,对ThreadLocal本身API的介绍简略带过。
读ThreadLocal源码,不读ThreadLocalMap的实现,和没看过没多大差别。
2. 两个问题
先回答两个问题:
-
什么是ThreadLocal?
ThreadLocal类顾名思义可以理解为线程本地变量。也就是说如果定义了一个ThreadLocal,每个线程往这个ThreadLocal中读写是线程隔离,互相之间不会影响的。它提供了一种将可变数据通过每个线程有自己的独立副本从而实现线程封闭的机制。 -
它大致的实现思路是怎样的?
Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。每个线程在往某个ThreadLocal里塞值的时候,都会往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
3. ThreadLocal的API
ThreadLocal的API其实没多少好介绍的,这些API介绍网上早已烂大街了。
4. ThreadLocalMap的源码实现
ThreadLocalMap的源码实现,才是我们读ThreadLocal源码真正要领悟的。看看大师Doug Lea和Joshua Bloch的鬼斧神工之作。
ThreadLocalMap提供了一种为ThreadLocal定制的高效实现,并且自带一种基于弱引用的垃圾清理机制。
下面从基本结构开始一点点解读。
4.1 存储结构
既然是个map(注意不要与java.util.map混为一谈,这里指的是概念上的map),当然得要有自己的key和value,上面回答的问题2中也已经提及,我们可以将其简单视作key为ThreadLocal,value为实际放入的值。之所以说是简单视作,因为实际上ThreadLocal中存放的是ThreadLocal的弱引用。我们来看看ThreadLocalMap里的节点是如何定义的。
static class Entry extends WeakReference<java.lang.ThreadLocal<?>> {
// 往ThreadLocal里实际塞入的值
Object value;
Entry(java.lang.ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry便是ThreadLocalMap里定义的节点,它继承了WeakReference类,定义了一个类型为Object的value,用于存放塞到ThreadLocal里的值。
4.2 为什么要弱引用
读到这里,如果不问不答为什么是这样的定义形式,为什么要用弱引用,等于没读懂源码。
因为如果这里使用普通的key-value形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。弱引用是Java中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次GC。当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利。
4.3 类成员变量与相应方法
/**
* 初始容量,必须为2的幂
*/
private static final int INITIAL_CAPACITY = 16;
/**
* Entry表,大小必须为2的幂
*/
private Entry[] table;
/**
* 表里entry的个数
*/
private int size = 0;
/**
* 重新分配表大小的阈值,默认为0
*/
private int threshold;
可以看到,ThreadLocalMap维护了一个Entry表或者说Entry数组,并且要求表的大小必须为2的幂,同时记录表里面entry的个数以及下一次需要扩容的阈值。
显然这里会产生一个问题,**为什么必须是2的幂?**很好,但是目前还无法回答,带着问题接着往下读。
/**
* 设置resize阈值以维持最坏2/3的装载因子
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/**
* 环形意义的下一个索引
*/
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);
}
ThreadLocal需要维持一个最坏2/3的负载因子,对于负载因子相信应该不会陌生,在HashMap中就有这个概念。
ThreadLocal有两个方法用于得到上一个/下一个索引,注意这里实际上是环形意义下的上一个与下一个。
由于ThreadLocalMap使用线性探测法来解决散列冲突,所以实际上Entry[]数组在程序逻辑上是作为一个环形存在的。
关于开放寻址、线性探测等内容,可以参考网上资料或者TAOCP(《计算机程序设计艺术》)第三卷的6.4章节。
至此,我们已经可以大致勾勒出ThreadLocalMap的内部存储结构。下面是我绘制的示意图。虚线表示弱引用,实线表示强引用。
ThreadLocalMap维护了Entry环形数组,数组中元素Entry的逻辑上的key为某个ThreadLocal对象(实际上是指向该ThreadLocal对象的弱引用),value为代码中该线程往该ThreadLoacl变量实际塞入的值。
4.4 构造函数
好的,接下来再来看看ThreadLocalMap的一个构造函数
/**
* 构造一个包含firstKey和firstValue的map。
* ThreadLocalMap是惰性构造的,所以只有当至少要往里面放一个元素的时候才会构建它。
*/
ThreadLocalMap(java.lang.ThreadLocal<?> firstKey, Object firstValue) {
// 初始化table数组
table = new Entry[INITIAL_CAPACITY];
// 用firstKey的threadLocalHashCode与初始大小16取模得到哈希值
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 初始化该节点
table[i] = new Entry(firstKey, firstValue);
// 设置节点表大小为1
size = 1;
// 设定扩容阈值
setThreshold(INITIAL_CAPACITY);
}
这个构造函数在set和get的时候都可能会被间接调用以初始化线程的ThreadLocalMap。
4.5 哈希函数
重点看一下上面构造函数中的int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
这一行代码。
ThreadLocal类中有一个被final修饰的类型为int的threadLocalHashCode,它在该ThreadLocal被构造的时候就会生成,相当于一个ThreadLocal的ID,而它的值来源于
/*
* 生成hash code间隙为这个魔数,可以让生成出来的值或者说ThreadLocal的ID较为均匀地分布在2的幂大小的数组中。
*/
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
可以看出,它是在上一个被构造出的ThreadLocal的ID/threadLocalHashCode的基础上加上一个魔数0x61c88647的。这个魔数的选取与斐波那契散列有关,0x61c88647对应的十进制为1640531527。斐波那契散列的乘数可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769,如果把这个值给转为带符号的int,则会得到-1640531527。换句话说(1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))
得到的结果就是1640531527也就是0x61c88647。通过理论与实践,当我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀。
ThreadLocalMap使用的是线性探测法,均匀分布的好处在于很快就能探测到下一个临近的可用slot,从而保证效率。这就回答了上文抛出的为什么大小要为2的幂的问题。为了优化效率。
对于& (INITIAL_CAPACITY - 1)
,相信有过算法竞赛经验或是阅读源码较多的程序员,一看就明白,对于2的幂作为模数取模,可以用&(2n-1)来替代%2n,位运算比取模效率高很多。至于为什么,因为对2^n取模,只要不是低n位对结果的贡献显然都是0,会影响结果的只能是低n位。
可以说在ThreadLocalMap中,形如key.threadLocalHashCode & (table.length - 1)
(其中key为一个ThreadLocal实例)这样的代码片段实质上就是在求一个ThreadLocal实例的哈希值,只是在源码实现中没有将其抽为一个公用函数。
4.6 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;
/*
* 在原代码的这里有句注释值得一提,原注释如下:
*
* Unlike Knuth 6.4 Algorithm R, we must scan until
* null because multiple entries could have been stale.
*
* 这段话提及了Knuth高德纳的著作TAOCP(《计算机程序设计艺术》)的6.4章节(散列)
* 中的R算法。R算法描述了如何从使用线性探测的散列表中删除一个元素。
* R算法维护了一个上次删除元素的index,当在非空连续段中扫到某个entry的哈希值取模后的索引
* 还没有遍历到时,会将该entry挪到index那个位置,并更新当前位置为新的index,
* 继续向后扫描直到遇到空的entry。
*
* ThreadLocalMap因为使用了弱引用,所以其实每个slot的状态有三种也即
* 有效(value未回收),无效(value已回收),空(entry==null)。
* 正是因为ThreadLocalMap的entry有三种状态,所以不能完全套高德纳原书的R算法。
*
* 因为expungeStaleEntry函数在扫描过程中还会对无效slot清理将之转为空slot,
* 如果直接套用R算法,可能会出现具有相同哈希值的entry之间断开(中间有空entry)。
*/
while (tab[h] != null) {
h = nextIndex(h, len);
}
tab[h] = e;
}
}
}
// 返回staleSlot之后第一个空的slot索引
return i;
}
我们来回顾一下从ThreadLocal读一个值可能遇到的情况:
根据入参threadLocal的threadLocalHashCode对表容量取模得到index
-
如果index对应的slot就是要读的threadLocal,则直接返回结果
-
调用getEntryAfterMiss线性探测,过程中每碰到无效slot,调用expungeStaleEntry进行段清理;如果找到了key,则返回结果entry
-
没有找到key,返回null
4.7 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;
}
我们来回顾一下ThreadLocal的set方法可能会有的情况
-
探测过程中slot都不无效,并且顺利找到key所在的slot,直接替换即可
-
探测过程中发现有无效slot,调用replaceStaleEntry,效果是最终一定会把key和value放在这个slot,并且会尽可能清理无效slot
-
在replaceStaleEntry过程中,如果找到了key,则做一个swap把它放到那个无效slot中,value置为新值
-
在replaceStaleEntry过程中,没有找到key,直接在无效slot原地放entry
-
-
探测没有发现key,则在连续段末尾的后一个空位置放上entry,这也是线性探测法的一部分。放完后,做一次启发式清理,如果没清理出去key,并且当前table大小已经超过阈值了,则做一次rehash,rehash函数会调用一次全量清理slot方法也即expungeStaleEntries,如果完了之后table大小超过了threshold - threshold / 4,则进行扩容2倍
4.8 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;
}
}
}
remove方法相对于getEntry和set方法比较简单,直接在table中找key,如果找到了,把弱引用断了做一次段清理。
5. ThreadLocal与内存泄漏
关于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。
6. InheritableThreadLocal原理
对于InheritableThreadLocal,本文不作过多介绍,只是简单略过。
ThreadLocal本身是线程隔离的,InheritableThreadLocal提供了一种父子线程之间的数据共享机制。
它的具体实现是在Thread类中除了threadLocals外还有一个inheritableThreadLocals
对象。
在线程对象初始化的时候,会调用ThreadLocal的createInheritedMap
从父线程的inheritableThreadLocals
中把有效的entry都拷过来
可以看一下其中的具体实现
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
// 这里的childValue方法在InheritableThreadLocal中默认实现为返回本身值,可以被重写
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
还是比较简单的,做的事情就是以父线程的inheritableThreadLocalMap
为数据源,过滤出有效的entry,初始化到自己的inheritableThreadLocalMap
中。其中childValue可以被重写。
需要注意的地方是InheritableThreadLocal
只是在子线程创建的时候会去拷一份父线程的inheritableThreadLocals
。如果父线程是在子线程创建后再set某个InheritableThreadLocal对象的值,对子线程是不可见的。
对于Java ThreadLocal的理解与应用场景分析
最常见的ThreadLocal使用场景为 用来解决数据库连接
一、对ThreadLocal理解
ThreadLocal提供一个方便的方式,可以根据不同的线程存放一些不同的特征属性,可以方便的在线程中进行存取。
二、以session为例来理解ThreadLocal
在web开发的session中,不同的线程对应不同的session,那么如何针对不同的线程获取对应的session呢?
我们可以设想了如下两种方式:
1.在action中创建session,然后传递给Service,Service再传递给Dao,很明显,这种方式将使代码变得臃肿复杂。
2.创建一个静态的map,键对应我们的线程,值对应session,当我们想获取session时,只需要获取map,然后根据当前的线程就可以获取对应的值。
我们看看Hibernate中是如何实现这种情况的:
在Hibernate中是通过使用ThreadLocal来实现的。在getSession方法中,如果ThreadLocal存在session,则返回session,否则创建一个session放入ThreadLocal中。
总结一下就是在ThreadLocal中存放了一个session。
为什么我们在ThreadLocal存放一个session,这个session就会与一个线程对应呢?
实际上ThreadLocal中并没有存放任何的对象或引用,在上面的的代码中ThreadLocal的实例threadSession只相当于一个标记的作用。而存放对象的真正位置是正在运行的Thread线程对象,每个Thread对象中都存放着一个ThreadLocalMap类型threadLocals对象,这是一个映射表map,这个map的键是一个ThreadLocal对象,值就是我们想存的局部对象。
我们以上面的代码为例分析一下:
当我们往ThreadLocal中存放变量的时候发生了什么?
即这行代码时。
我们看下ThreadLocal的源码中set()方法的实现。
如果把这些代码简化的话就一句
Thread.currentThread().threadLocals.set(this,value);
Thread.currentThread()获取当前的线程
threadLocals就是我们上面说的每个线程对象中用于存放局部对象的map
所以set()就是获取到当前线程的map然后把值放进去,我们发现键是this,也就是当前的ThreadLocal对象,可以发现ThreadLocal对象就是一个标记的作用,我们根据这个标记找到对应的局部对象。
如果对比get()方法,可以发现原理都差不多,都是对线程中的threadLocals这个map的操作,我就不解释了。
ThreadLocal就是一个标记的作用,当我们在线程中使用ThreadLocal的set()或者get()方法时,其实是在操作我们线程自带的threadLocals这个map,多个线程的时候自然就有多个map,这些map互相独立,但是,这些map都是根据一个ThreadLocal对象(因为它是静态的)来作为键存放。
这样可以在多个线程中,每个线程存放不一样的变量,我们通过一个ThreadLocal对象,在不同的线程(通过Thread.currentThread()获取当前线程)中得到不同的值(不同线程的threadLocals不一样)。
为什么threadLocals要是一个map呢?
因为我们可能会在一个类中声明多个ThreadLocal的实例,这样就有多个标记,所以要使用map对应。
总结:
ThreadLocal就是用来在类中声明的一个标记,然后通过这个标记就根据不同Thread对象存取值。
应用场景:
在线程中存放一些就像session的这种特征变量,会针对不同的线程,有不同的值。
ThreadLocal是什么
ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。
从数据结构入手
下图为ThreadLocal的内部结构图
ThreadLocal结构内部
从上面的结构图,我们已经窥见ThreadLocal的核心机制:
- 每个Thread线程内部都有一个Map。
- Map里面存储线程本地对象(key)和线程的变量副本(value)
- 但是,Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
Thread线程内部的Map在类中描述如下:
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
深入解析ThreadLocal
ThreadLocal类提供如下几个核心方法:
public T get()public void set(T value)public void initialValue()public void remove()
- get()方法用于获取当前线程的副本变量值。
- set()方法用于保存当前线程的副本变量值。
- initialValue()为当前线程初始副本变量值。
- remove()方法移除当前前程的副本变量值。
get()方法
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
步骤:
1.获取当前线程的ThreadLocalMap对象threadLocals
2.从map中获取线程存储的K-V Entry节点。
3.从Entry节点获取存储的Value副本值返回。
4.map为空的话返回初始值null,即线程变量副本为null,在使用时需要注意判断NullPointerException。
set()方法
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/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;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
步骤:
1.获取当前线程的成员变量map
2.map非空,则重新将ThreadLocal和新的value副本放入到map中。
3.map空,则对线程的成员变量ThreadLocalMap进行初始化创建,并将ThreadLocal和value副本放入map中。
remove()方法
/**
* Removes the current thread's value for this thread-local
* variable. If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim. This may result in multiple invocations of the
* <tt>initialValue</tt> method in the current thread.
*
* @since 1.5
*/public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
remove方法比较简单,不做赘述。
ThreadLocalMap
ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也独立实现。
ThreadLocalMap类图
在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。但是Entry中key只能是ThreadLocal对象,这点被Entry的构造方法已经限定死了。
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
Entry继承自WeakReference(弱引用,生命周期只能存活到下次GC前),但只有Key是弱引用类型的,Value并非弱引用。
ThreadLocalMap的成员变量:
static class ThreadLocalMap {
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
/**
* The number of entries in the table.
*/
private int size = 0;
/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0
}
Hash冲突怎么解决
和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。
/**
* Increment i modulo len.
*/private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
*/private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。
所以这里引出的良好建议是:每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。
由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
如何避免泄漏
既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。
如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。
ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();try {
threadLocal.set(new Session(1, "Misout的博客"));
// 其它业务逻辑
} finally {
threadLocal.remove();
}
应用场景
还记得Hibernate的session获取场景吗?
private static final ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();
//获取Sessionpublic static Session getCurrentSession(){
Session session = threadLocal.get();
//判断Session是否为空,如果为空,将创建一个session,并设置到本地线程变量中
try {
if(session ==null&&!session.isOpen()){
if(sessionFactory==null){
rbuildSessionFactory();// 创建Hibernate的SessionFactory
}else{
session = sessionFactory.openSession();
}
}
threadLocal.set(session);
} catch (Exception e) {
// TODO: handle exception
}
return session;
}
为什么?每个线程访问数据库都应当是一个独立的Session会话,如果多个线程共享同一个Session会话,有可能其他线程关闭连接了,当前线程再执行提交时就会出现会话已关闭的异常,导致系统异常。此方式能避免线程争抢Session,提高并发下的安全性。
使用ThreadLocal的典型场景正如上面的数据库连接管理,线程会话管理等场景,只适用于独立变量副本的情况,如果变量为全局共享的,则不适用在高并发下使用。
总结
- 每个ThreadLocal只能保存一个变量副本,如果想要上线一个线程能够保存多个副本以上,就需要创建多个ThreadLocal。
- ThreadLocal内部的ThreadLocalMap键为弱引用,会有内存泄漏的风险。
- 适用于无状态,副本变量独立后不影响业务逻辑的高并发场景。如果如果业务逻辑强依赖于副本变量,则不适合用ThreadLocal解决,需要另寻解决方案。