介绍
- 顾名思义这个类提供线程局部变量
- 每个线程(通过其get或set方法)都有自己独立初始化的变量副本
ThreadLocal思想
在多线程环境下,不同的线程同时访问同一个共享变量会有并发问题。一种解决方法是进行同步,例如使用synchronized。另外一种比较常见的形式就是局部(local)变量(这里排除局部变量引用指向共享对象的情况),这样资源就不是被两个线程共享,那么也不会出现竞争问题。
自定义类实现ThreadLocal的功能
一个简单的思路是使用 Map 存储每个变量的副本,将当前线程的 Name 作为 key,副本变量作为 value 值:
public class Test {
/** 用于存储每个线程对应的数据 */
public static class CustomThreadLocal{
public final Map<String,Integer> cacheValueMap=new HashMap<String, Integer>();
private int defaultValue;
public CustomThreadLocal(int value){
defaultValue=value;
}
public void set(Integer value){
cacheValueMap.put(Thread.currentThread().getName(),value);
}
public Integer get(){
String threadName=Thread.currentThread().getName();
if(cacheValueMap.containsKey(threadName)){
return cacheValueMap.get(threadName);
}
return defaultValue;
}
}
/** 数据资源类,提供进行加减操作*/
public static class Number {
private CustomThreadLocal value = new CustomThreadLocal(0);
public void increase() throws InterruptedException {
value.set(10);
Thread.sleep(10);
System.out.println("increase value: " + value.get());
}
public void decrease() throws InterruptedException {
value.set(-10);
Thread.sleep(10);
System.out.println("decrease value: " + value.get());
}
}
public static void main(String[] args) {
final Number number=new Number();
Thread increaseThread=new Thread(new Runnable() {
public void run() {
try {
number.increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"th1");
Thread decreaseThread=new Thread(new Runnable() {
public void run() {
try {
number.decrease();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"th2");
increaseThread.start();
decreaseThread.start();
}
}
这种写法存在的问题:
- 即便线程执行完,只要 number 变量存在,线程的副本变量依然会存在(存放在 number 的 cacheMap 中)。
- 多个线程有可能会同时操作 cacheMap,需要对 cacheMap 进行同步处理
为了解决上面的问题,我们换种思路,每个线程创建一个 Map,存放当前线程中副本变量,用 CustomThreadLocal 的实例作为 key 值,下面是一个示例:
public class Test {
/** 自定义线程,并定义map存放当前线程中副本变量 */
public static class ManualThread extends Thread{
public final Map<Integer,Integer> cacheValueMap=new HashMap<Integer, Integer>();
}
/** 通过实例本身映射出线程的副本数据,并对其进行操作 */
public static class CustomThreadLocal{
private int defaultValue;
public CustomThreadLocal(int value){
defaultValue=value;
}
public void set(Integer value){
Integer id = this.hashCode();
Map<Integer, Integer> cacheMap = getMap();
cacheMap.put(id, value);
}
public Integer get(){
Integer id = this.hashCode();
Map<Integer, Integer> cacheMap = getMap();
if (cacheMap.containsKey(id)) {
return cacheMap.get(id);
}
return defaultValue;
}
//注意这个方法
public Map<Integer, Integer> getMap() {
ManualThread thread = (ManualThread) Thread.currentThread();
return thread.cacheValueMap;
}
}
/** 数据资源类,提供进行加减操作*/
public static class Number {
private CustomThreadLocal value = new CustomThreadLocal(0);
public void increase() throws InterruptedException {
value.set(10);
Thread.sleep(10);
System.out.println("increase value: " + value.get());
}
public void decrease() throws InterruptedException {
value.set(-10);
Thread.sleep(10);
System.out.println("decrease value: " + value.get());
}
}
public static void main(String[] args) {
final Number number=new Number();
//使用自定义线程类
Thread increaseThread=new ManualThread(){
public void run() {
try {
number.increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread decreaseThread=new ManualThread(){
public void run() {
try {
number.decrease();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
increaseThread.start();
decreaseThread.start();
}
}
这种写法,当线程消亡之后,线程中存放的副本变量也会被全部回收,并且 cacheMap 是线程私有的,不会出现多个线程并发问题。在 Java 中,ThreadLocal 类的实现就是采用的这种思想,注意只是思想,实际的实现和上面的并不一样。
基本原理
ThreadLocal 的实现思想,我们在前面已经说了,每个线程维护一个 ThreadLocalMap 的映射表,映射表的 key 是 ThreadLocal 实例本身,value 是要存储的副本变量。ThreadLocal 实例本身并不存储值,它只是提供一个在当前线程中找到副本值的 key。 如下图所示:
API总览
get函数用来获取与当前线程关联的ThreadLocal的值,如果当前线程没有该ThreadLocal的值,则调用initialValue函数获取初始值返回,initialValue是protected类型的,所以一般我们使用时需要继承该函数,给出初始值。而set函数是用来设置当前线程的该ThreadLocal的值,remove函数用来删除ThreadLocal绑定的值,在某些情况下需要手动调用,防止内存泄露
关键点分析
ThreadLocal 散列值
当创建了一个 ThreadLocal 的实例后,它的散列值就已经确定了,下面是 ThreadLocal 中的实现
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode =new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
我们看到 threadLocalHashCode 是一个常量,它通过 nextHashCode() 函数产生。nextHashCode() 函数其实就是在一个 AtomicInteger 变量(初始值为0)的基础上每次累加 0x61c88647,使用 AtomicInteger 为了保证每次的加法是原子操作。而 0x61c88647 这个就比较神奇了,它可以使 hashcode 均匀的分布在大小为 2 的 N 次方的数组里。
具体散列测试请看https://www.jianshu.com/p/fe9ffcf51f4b
ThreadLocalMap
被定义为一个静态类,包含的主要成员:
- 首先是Entry的定义;
- 初始的容量为INITIAL_CAPACITY = 16;
- 主要数据结构就是一个Entry的数组table;
- size用于记录Map中实际存在的entry个数;
- threshold是扩容上限,当size到达threashold时,需要resize整个Map,threshold的初始值为len * 2 / 3;
- nextIndex和prevIndex则是为了安全的移动索引
Entry
- ThreadLocalMap 使用 Entry 类来存储数据
- entry的key是ThreadLocal实例,value是Object(即我们所谓的“线程本地数据”)
- 为避免占用空间较大或生命周期较长的数据常驻于内存引发一系列问题,hash table的key是弱引用WeakReferences
static class Entry extends WeakReference <ThreadLocal <?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal <?> k, Object v) {
super(k);
value = v;
}
}
set 函数
private void set(ThreadLocal <?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置
int i = key.threadLocalHashCode & (len - 1);
// 使用线性探测法查找元素
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal <?> k = e.get();
// ThreadLocal 对应的 key 存在,直接覆盖之前的值
if (k == key) {
e.value = value;
return;
}
// key为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了,当前数组中的 Entry 是一个陈旧(stale)的元素
if (k == null) {
// 用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏
replaceStaleEntry(key, value, i);
return;
}
}
// ThreadLocal 对应的 key 不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的 Entry。
tab[i] = new Entry(key, value);
int sz = ++size;
// cleanSomeSlot 清理陈旧的 Entry(key == null),具体的参考源码。如果没有清理陈旧的 Entry 并且数组中的元素大于了阈值,则进行 rehash。
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
set函数注意点
- int i = key.threadLocalHashCode & (len - 1);,这里实际上是对 len-1 进行了取余操作。之所以能这样取余是因为 len 的值比较特殊,是 2 的 n 次方,减 1 之后低位变为全 1,高位变为全 0。例如 16,减 1 之后对应的二进制为: 00001111,这样其他数字中大于 16 的部分就会被 0 与掉,小于 16 的部分就会保留下来,就相当于取余了。
- 在 replaceStaleEntry 和 cleanSomeSlots 方法中都会清理一些陈旧的 Entry,防止内存泄漏
threshold 的值大小为 threshold = len * 2 / 3;- rehash 方法中首先会清理陈旧的 Entry,如果清理完之后元素数量仍然大于 threshold 的 3/4,则进行扩容操作(数组大小变为原来的 2倍)
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
getEntry函数
private Entry getEntry(ThreadLocal <?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
因为 ThreadLocalMap 中采用开放定址法,所以当前 key 的散列值和元素在数组中的索引并不一定完全对应。所以在 get 的时候,首先会看 key 的散列值对应的数组元素是否为要查找的元素,如果不是,再调用getEntryAfterMiss方法查找后面的元素
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)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
所以首先e如果为null的话,那么getEntryAfterMiss
还是直接返回null的,如果是不满足e.get() == key
,那么进入while循环,这里是不断循环,如果e一直不为空,那么就调用nextIndex,不断递增i,在此过程中一直会做两个判断:
- 如果
k==key
,那么代表找到了这个所需要的Entry,直接返回; - 如果
k==null
,那么证明这个Entry中key已经为null,那么这个Entry就是一个过期对象,这里调用expungeStaleEntry
清理该Entry。 这里就解答了导致内存泄露的原因,即ThreadLocal Ref销毁时,ThreadLocal实例由于只有Entry中的一条弱引用指着,那么就会被GC掉,Entry的key没了,value可能会内存泄露的,其实在每一个get,set操作时都会不断清理掉这种key为null的Entry的。
为什么循环查找
主要是因为处理哈希冲突的方法,我们都知道HashMap采用拉链法处理哈希冲突,即在一个位置已经有元素了,就采用链表把冲突的元素链接在该元素后面,而ThreadLocal采用的是开放地址法,即有冲突后,把要插入的元素放在要插入的位置后面为null的地方,具体关于这两种方法的区别可以参考:解决哈希(HASH)冲突的主要方法。所以上面的循环就是因为我们在第一次计算出来的i位置不一定存在key与我们想查找的key恰好相等的Entry,所以只能不断在后面循环,来查找是不是被插到后面了,直到找到为null的元素,因为若是插入也是到null为止的。
分析完循环的原因,其实也可以深入expungeStaleEntry看看是怎么清理的。
expungeStaleEntry
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;
}
看上面这段代码主要有两部分:
- expunge entry at staleSlot:这段主要是将i位置上的Entry的value设为null,Entry的引用也设为null,那么系统GC的时候自然会清理掉这块内存;
- Rehash until we encounter null: 这段就是扫描位置staleSlot之后,null之前的Entry数组,清除每一个key为null的Entry,同时若是key不为空,做rehash,调整其位置。
为什么要做rehash呢
因为我们在清理的过程中会把某个值设为null,那么这个值后面的区域如果之前是连着前面的,那么下次循环查找时,就会只查到null为止。
举个例子就是:...,<key1(hash1), value1>, <key2(hash1), value2>,...(即key1和key2的hash值相同) 此时,若插入<key3(hash2), value3>,其hash计算的目标位置被<key2(hash1), value2>占了,于是往后寻找可用位置,hash表可能变为: ..., <key1(hash1), value1>, <key2(hash1), value2>, <key3(hash2), value3>, ... 此时,若<key2(hash1), value2>被清理,显然<key3(hash2), value3>应该往前移(即通过rehash调整位置),否则若以key3查找hash表,将会找不到key3
remove方法
删除其实就是将 Entry 的键值设为 null,变为陈旧的 Entry。然后调用 expungeStaleEntry 清理陈旧的 Entry
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的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);
}
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;
}
}
return setInitialValue();
}
存取的基本流程就是首先获得当前线程的 ThreadLocalMap,将 ThreadLocal 实例作为键值传入 Map,然后就是进行相关的变量存取工作了。线程中的 ThreadLocalMap 是懒加载的,只有真正的要存变量时才会调用 createMap 创建,下面是 createMap 的实现:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
如果想要给 ThreadLocal 的副本变量设置初始值,需要重写 initialValue 方法,如下面的形式:
ThreadLocal <Integer> threadLocal = new ThreadLocal() {
protected Integer initialValue() {
return 0;
}
};
SuppliedThreadLocal
SuppliedThreadLocal是JDK8新增的内部类,只是扩展了ThreadLocal的初始化值的方法而已,允许使用JDK8新增的Lambda表达式赋值。需要注意的是,函数式接口Supplier不允许为null
static final class SuppliedThreadLocal<T>extends ThreadLocal<T>{
private final Supplier<?extends T> supplier;
SuppliedThreadLocal(Supplier<?extends T> supplier){
this.supplier= Objects.requireNonNull(supplier);
}
@Override
protected T initialValue(){
return supplier.get();
}
}
总结
为什么会内存泄漏
ThreadLocalMap
使用ThreadLocal
的弱引用作为key
,如果一个ThreadLocal
没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal
势必会被回收,这样一来,ThreadLocalMap
中就会出现key
为null
的Entry
,就没有办法访问这些key
为null
的Entry
的value
,如果当前线程再迟迟不结束的话,这些key
为null
的Entry
的value
就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄漏。
怎么避免内存泄漏
每次使用完ThreadLocal,都调用它的remove()方法,清除数据
为什么没有next
ThreadLocalMap 中使用开放地址法来处理散列冲突,而 HashMap 中使用的分离链表法。之所以采用不同的方式主要是因为:在 ThreadLocalMap 中的散列值分散的十分均匀,很少会出现冲突。并且 ThreadLocalMap 经常需要清除无用的对象,使用纯数组更加方便。所以不需要next
为什么使用弱引用
因为如果这里使用普通的key-value形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。弱引用是Java中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次GC。当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利。
如何清理entry
调用expungeStaleEntry
进行实现
使用场景及方式
- 主要应用场景为按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到,例如数据库连接、Session管理、日志的uniqueID等
使用注意事项
- ThreadLocal实例通常来说都是private static类型
- ThreadLocal并未解决多线程访问共享对象的问题,而是为每个线程创建一个单独的变量副本,提供了保持对象的方法和避免参数传递的复杂性;
- ThreadLocal并不是每个线程拷贝一个对象,而是直接new(新建)一个;
- 如果ThreadLocal.set()的对象是多线程共享的,那么还是涉及并发问题。
- 过度使用ThreadLocal很容易加大类之间的耦合度与依赖关系(开发过程可能会不得不过度考虑某个ThreadLocal在调用时是否已有值,存放的是哪个类放的什么值)
- 应用一定要自己负责 remove,并且不要和线程池配合,因为woker线程往往是不会退出的。
参考地址
http://blog.zhangjikai.com/2017/03/29/%E3%80%90Java-%E5%B9%B6%E5%8F%91%E3%80%91%E8%AF%A6%E8%A7%A3-ThreadLocal
http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/
https://juejin.im/post/5a5efb1b518825732b19dca4#heading-9
https://www.cnblogs.com/micrari/p/6790229.html