ThreadLocal的源码解析以及内存泄漏的原理分析
介绍了Java中的ThreadLocal的作用、原理、源码以及应用,并且介绍了ThreadLocal的内存泄漏的原理以及解决办法。
1 ThreadLocal的概述
1.1 ThreadLocal的入门
public class ThreadLocal< T > extends Object
ThreadLocal来自JDK1.2,位于java.lang包。ThreadLocal可以提供线程内的局部变量,这种变量在线程的生命周期内起作用,ThreadLocal又叫做线程本地变量/线程本地存储。
实际上,单就ThreadLocal这个类来说,它不存储任何内容,真正存储数据的集合在每个Thread中的threadLocals变量里面,ThreadLocal中只是定义了这个集合的结构,并提供了一系列操作的方法。后面的源码分析处会讲到!
可以说,ThreadLocal只是一个工具类,一个对各个线程的threadLocals进行操作的工具而已。
ThreadLocal 的作用和目的:
- 用于实现线程内的数据共享,某些数据是以线程为作用域并且不同线程具有不同的数据副本时,即数据在线程之间隔离,就可以考虑用ThreadLocal。即对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。
- 方便同一个线程复杂逻辑下的数据传递,有些时候一个线程中的任务过于复杂,我们又需要某个数据能够贯穿整个线程的执行过程,可能涉及到不同类/函数之间数据的传递。此时使用Threadlocal存放数据,在线程内部只要通过get方法就可以获取到在该线程中存进去的数据,方便快捷。
1.2 同步和ThreadLocal
同步与ThreadLocal是解决多线程中数据访问问题的两种思路,前者是数据共享的思路,后者是数据隔离的思路。同步是一种以时间换空间的思想,ThreadLocal是一种空间换时间的思想。前者仅提供一份变量,让不同的线程排队访问,实现串行化;而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
Threadlocal并不能代替同步,注意ThreadLocal不是用来解决共享对象的多线程访问问题的。通过ThreadLocal的set()方法设置到线程的threadLocals里的是线程自己要存储的对象,其他线程不需要去访问,也是访问不到的。各个线程中的threadLocals以及里面的值都是不同的对象。Threadloocal是用来进行变量隔离,就是说ThreadLocal是针对那些不需要共享的属性!
1.3 主要API方法与使用案例
ThreadLocal类主要有四个可供调用的方法:
- void set(T value):保存值;
- T get():获取值;
- void remove():移除值;
- initialValue():返回该线程局部变量的初始值,该方法是为了让子类继承而设计的。这个方法是一个延迟调用方法,在一个线程第一次调用get()时(并且set未被调用)才执行。ThreadLocal中的默认实现是直接返回一个null。
ThreadLocal实现线程内数据共享,线程间数据隔离的案例:
public class ThreadLocalTest {
/**
* 全局ThreadLocal对象位于堆中,这是线程共享的,而方法栈,是每个线程私有的
*/
static ThreadLocal<String> th = new ThreadLocal<>();
public static void set() {
//设置值,值为当前线程的名字
th.set(Thread.currentThread().getName());
}
public static String get() {
//获取值
return th.get();
}
public static void main(String[] args) throws InterruptedException {
System.out.println("主线程中尝试获取值:" + get());
//主线程中设置值,值为线程名字
set();
//主线程中尝试获取值
System.out.println("主线程中再次尝试获取值:" + get());
//开启一条子线程
Thread thread = new Thread(new Th1(), "child");
thread.start();
//主线程等待子线程执行完毕
thread.join();
System.out.println("等待子线程执行完毕,主线程中再次尝试获取值:" + get());
}
static class Th1 implements Runnable {
@Override
public void run() {
System.out.println("子线程中尝试获取值:" + get());
//子线程中设置值,值为线程名字
set();
System.out.println("子线程中再次尝试获取值:" + get());
}
}
}
结果如下:
主线程中尝试获取值:null
主线程中再次尝试获取值:main
子线程中尝试获取值:null
子线程中再次尝试获取值:child
等待子线程执行完毕,主线程中再次尝试获取值:main
先设置值,然后获取,可以得到“main”。然后开启子线程,在子线程内部,先获取,得到null,然后设置值,再获取,得到“child”。最后在主线程中再尝试获取,得到的还是原值“main”,这说明ThreadLocal使得变量的作用范围限制在本线程中了,其他线程是无法访问到该变量的。
注意这里由于案例演示在最后并没有调用remove方法,在实际使用中应该在使用完毕之后调用remove方法,原理后面会讲!
2 ThreadLocal的原理
2.1 基本关系
ThreadLocal类中定义了一个内部类ThreadLocalMap,ThreadLocalMap是真正存放数据的容器,实际上它的底层就是一张哈希表。
每个Thread线程内部都定义有一个ThreadLocal.ThreadLocalMap
类型的threadLocals
变量,这样,线程之间的ThreadLocalMap互不干扰。threadLocals
变量持有的ThreadLocalMap在ThreadLocal调用set或者get方法时才会初始化。
ThreadLocal还提供相关方法,负责向当前线程的ThreadLocalMap变量获取和设置线程的变量值,相当于一个工具类。
当在某个线程的方法中使用ThreadLocal设置值的时候,就会将该ThreadLocal对象添加到该线程内部的ThreadLocalMap中,其中键就是该ThreadLocal对象,值可以是任意类型任意值。当在某个线程的方法中使用ThreadLocal获取值的时候,会以该ThreadLocal对象为键,在该线程的ThreadLocalMap中获取对应的值。
ThreadLocal中定义了ThreadLocalMap的结构,并提供操作的方法:
public class ThreadLocal<T> {
//……
static class ThreadLocalMap {
//……
}
/**
* ThreadLocal的构造器,里面什么都没有
* 创建ThreadLocal时,没有初始化ThreadLocalMap,在set、get方法中还可能初始化!
*/
public ThreadLocal() {
}
}
每个thread对象都持有一个ThreadLocalMap类型的引用变量,用于存放线程本地变量。key为ThreadLocal对象,value为要存储的数据。
public class Thread implements Runnable {
/*与此线程相关的线程本地值。此ThreadLocalMap定义在ThreadLocal类中,使用在Thread类中*/
ThreadLocal.ThreadLocalMap threadLocals = null;
//………………
}
下面是Thread、threadlocalMap、ThreadLocal的关系图:
2.2 基本结构
ThreadLocal中定义了ThreadLocalMap的结构。
ThreadLocalMap也是一张key-value类型的哈希表,但是ThreadLocalMap并没有实现Map接口,它内部具有一个Entry类型的table数组用于存放节点。Entry节点用于存放key、value数据,并且继承了WeakReference。
通过对该 ThreadLocal 对象进行哈希运算,可以得到该 ThreadLocal 对象在 Entry 数组中的桶位,从而找到唯一的 Entry。**如果发生了哈希冲突,那么与 HashMap 和 Hashtable 采用的“链地址法”不同,ThreadLocalMap 采用“线性探测法”解决哈希冲突,采用该方法的原因是实现很简单,占用更小的空间,并且一般来说一个ThreadLocalMap并不会存放很多数据!
在创建ThreadLocalMap对象的同时即初始化16个长度的内部table数组,扩容阈值为len * 2 / 3
,扩容增量为增加原容量的1倍
。
在没有使用ThreadLocal设置、获取值时,线程中的ThreadLocalMap对象一直为null。
/**
* ThreadLocal的内部类ThreadLocalMap
*/
static class ThreadLocalMap {
/**
* table数组的初始化容量,
*/
private static final int INITIAL_CAPACITY = 16;
//存放数据的数组,在创建ThreadLocalMap对象时将会初始化该数组,大小必须是2^N次方
private Entry[] table;
//扩容阈值,为len * 2 / 3
private int threshold;
/**
* 内部节点对象,貌似没找到“key”字段在哪里,实际上存放k的属性位于其父类WeakReference的父类Reference中,名为referent,即属性复用
* 插入数据时,通过对key(threadLocal对象)的hash计算,来找出Entry应该存放的table数组的桶位,
* 不过可能造成hash冲突,它采用线性探测法解决冲突,因此需要线性向后查找。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
//存放值
Object value;
//构造器
Entry(ThreadLocal<?> k, Object v) {
//调用父类的构造器,传入key,这里k被包装成为弱引用
//实际上存放k的属性位于其父类WeakReference的父类Reference中,名为referent,即属性复用
super(k);
value = v;
}
}
}
2.3 set方法
set方法是由ThreadLocal提供的,用于存放数据,大概步骤如下:
- 获取当前线程的成员变量threadLocals;
- 如果threadLocals不等于null,则调用set方法存放数据,方法结束;
- 否则,调用createMap方法初始化threadLocals,然后存放数据,方法结束。
/**
* ThreadLocal中的方法,开放给外部调用的存放数据的方法
*
* @param value 需要存放的数据
*/
public void set(T value) {
//注意,这里首先获取当前线程t
Thread t = Thread.currentThread();
//1.1 然后通过getMap方法,传入t,获取当前t线程的threadLocals
ThreadLocalMap map = getMap(t);
//如果map存在,则存放数据
if (map != null)
//this代指当前ThreadLocal对象,value表示值
map.set(this, value);
else
//如果不存在,则构建属于当前线程的ThreadLocalMap并存放数据
createMap(t, value);
}
/**
* 1.1 ThreadLocal中的方法,获取指定线程的threadLocals
*
* @param t 指定线程
* @return t的threadLocals
*/
ThreadLocalMap getMap(Thread t) {
//t代表当前线程,获取该线程的threadLocals属性,该属性就是一个ThreadLocalMap,默认为null
return t.threadLocals;
}
/**
* 1.2 ThreadLocal中的方法,用于构建ThreadLocalMap对象并赋值
*
* @param t 当前线程
* @param firstValue 要存入的值
*/
void createMap(Thread t, T firstValue) {
//该方法是ThreadLocal中的方法,this代指当前ThreadLocal对象
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
* 位于ThreadLocalMap中的构造器,用于创建新的ThreadLocalMap对象
*
* @param firstKey key
* @param firstValue value
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//创建table数组,初始容量为INITIAL_CAPACITY,即16
table = new Entry[INITIAL_CAPACITY];
//寻找数组桶位,通过ThreadLocal对象的threadLocalHashCode属性 & 15
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//该位置存放元素,由于是刚创建对象,因此不存在哈希冲突的情况,直接存储就行了
//构造器在“基本结构”部分分析过,key最终被包装成弱引用。
table[i] = new Entry(firstKey, firstValue);
//size设置为1
size = 1;
//setThreshold方法设置扩容阀值
setThreshold(INITIAL_CAPACITY);
}
/**
* ThreadLocalMap中的方法,设置扩容阈值
*
* @param len 数组长度
*/
private void setThreshold(int len) {
//数组长度的2/3
threshold = len * 2 / 3;
}
2.3.1 内部set方法
上面的set方法中,如果当前t线程的threadLocals不为null,那么又调用了另一个私有的set方法存放数据。该方法是ThreadLocal的核心方法之一,并且比较复杂,大概具有如下步骤:
- 通过哈希算法计算出当前key存放的桶位i,并获取i的元素e。
- 如果e不为空,说明发生哈希冲突,使用线性探测法替换或者存放数据:
- 如果e的key和指定key相等(使用==比较),那么替换value,方法结束;
- 否则,如果e的key等于null,那说明是无效数据。调用replaceStaleEntry从该索引开始清理无效数据,并且存放新数据,在replaceStaleEntry过程中:
- 如果找到了key相等的entry,则它放到无效桶位中,value置为新值,方法结束。
- 如果没找到key相等的entry,直接在无效slot原地放entry,方法结束。
- 调用到了replaceStaleEntry方法,那就肯定能将新数据存入ThreadLocalMap中,并且不再执行后续步骤。
- 否则,nextIndex方法获取下一个索引并赋值给i,如果该位置的节点e为null,则结束循环,否则进行下一次循环;
- 走到这一步,说明没有替换value,也没有没有进行无效数据清理,而是找到了一个空桶位i,直接在该位置插入新entry,此时肯定保证最初始的i和现在的之间的位置都是存在有效节点的;
- 存放元素完毕之后,再调用cleanSomeSlots做一次部分无效节点清理,如果没清理出去key(返回false)并且当前table大小 大于等于 阈值,则调用rehash方法;
- rehash方法中会调用一次全表扫描清理的方法即expungeStaleEntries()方法。如果expungeStaleEntries完毕之后table大小还是大于等于(threshold – threshold / 4),则调用resize方法进行扩容;
- resize方法将扩容两倍,同时完成节点的转移。
ThreadLocalMap使用==比较key是否相同。ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1(线性探测),寻找下一个相邻的位置。当向前寻找到数组头部或者向后寻找到数组尾部的时候,下一个位置就是数组尾部或者数组头部,即循环查找。
/**
* 位于ThreadLocalMap内的set方法,用于存放数据。
*
* @param key ThreadLocal对象
* @param value 值
*/
private void set(ThreadLocal<?> key, Object value) {
//tab保存数组引用
Entry[] tab = table;
//len保存数组的度
int len = tab.length;
/*1 哈希算法计算桶位 通过ThreadLocal的threadLocalHashCode属性计算出该key(ThreadLocal对象)对应的数组桶位i*/
int i = key.threadLocalHashCode & (len - 1);
/*
* 2 使用线性探测法存放元素,可能进行垃圾清理
* 获取i索引位置的Entry e,如果e不为null,说明发生了哈希冲突,下面开始解决:
* 判断两个key是否相等,即是否需要进行value替换,如果相等,则替换value,解决完毕,方法返回;
* 否则,判断获取的e的key是否为null,如果获取的ThreadLocal为null,这说明该WeakReference(弱引用)被回收了(因为Entry继承了WeakReference),
* 说明ThreadLocal肯定在外部没有强引用了,这个Entry变成了垃圾,调用replaceStaleEntry方法擦除该位置或者其他的无效的Entry,重新赋值,解决完毕,方法返回,这是为了防止内存泄漏
* 否则判断该位置i是否为null,即没有节点,如果为null,则在该位置新建节点并插入,解决完毕。
* 否则,i = nextIndex(i, len),尝试下一次循环。
* 如果循环完毕,方法还没结束,那说明没找到key相等的节点和key==null的节点,但是找到了下一个节点为null的桶位,记录此时索引值i,将会在该位置插入新节点。
*
* 这就是ThreadLocalMap解决哈希冲突的办法,即开放定址法——线性探测:当冲突时,向下查找下一个节点为null的位置存放新节点
* nextIndex()方法用于循环数组索引,即如果初始i为15,长度为16,那么nextIndex将返回0,如果初始i为1,长度为16,那么nextIndex将返回2。
* 这样的做法有利于利用起始索引前面的空间
* */
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
/*获取该Entry的key,即原来的ThreadLocal对象,这是其父类Reference的方法*/
ThreadLocal<?> k = e.get();
/*如果获取的ThreadLocal和要存的ThreadLocal是同一个对象,那么就替换值,方法结束
* 这里能够看出,判断key相等的条件是两个对象使用==比较返回true
* */
if (k == key) {
e.value = value;
return;
}
/*
*如果获取的ThreadLocal为null,这说明该WeakReference(弱引用)被回收了(因为Entry继承了WeakReference),
* 说明ThreadLocal肯定在外部没有强引用了,这个Entry变成了垃圾,擦除该位置的Entry,重新赋值并结束方法,这是为了防止内存泄漏*/
if (k == null) {
/*
* 从该位置开始,继续寻找key,并且会尽可能清理其他无效slot
* 在replaceStaleEntry过程中,如果找到了key,则做一个swap把它放到那个无效slot中,value置为新值
* 在replaceStaleEntry过程中,没有找到key,直接在该无效slot原地放entry
* */
replaceStaleEntry(key, value, i);
return;
}
}
/*
* 执行到这一步方法还没有返回,说明i位置没有节点,此时e等于null,直接在该位置插入新的Entry
* 此时肯定保证最初始的i和现在的之间的位置是存在节点的!
* */
tab[i] = new Entry(key, value);
//size自增1,使用sz记录
int sz = ++size;
/* 3 尝试清理垃圾,然后判断是否需要扩容,如果需要那就扩容
* 存放完毕元素之后,再调用cleanSomeSlots做一次垃圾清理,如果没清理出去key(返回false)
* 并且当前table大小大于等于阈值,则调用rehash方法
* rehash方法中会调用一次全量清理slot方法也即expungeStaleEntries()方法
* 如果expungeStaleEntries完毕之后table大小还是大于等于(threshold – threshold / 4),则调用resize方法进行扩容
* resize方法将扩容两倍,同时完成节点的转移
* */
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
/**
* 在length的索引范围内获取i的下一个索引,循环
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* 在length的索引范围内获取i的上一个索引,循环
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
2.3.2 哈希算法
在set方法中,我们可以找到ThreadLocalMap的哈希算法为:
int i = key.threadLocalHashCode & (len - 1);
由于len长度一定是2的幂次方,因此上面的位运算可以转换为key.threadLocalHashCode% len
,所以说ThreadLocalMap的哈希算法也是一种取模(求余)算法
,因为余数一定会比除数小,那么计算出来的桶位肯定是位于[0, len-1]之间了,刚好在底层数组的索引范围内,还是比较简单的。
这里的key我们知道是ThreadLocal对象,这个threadLocalHashCode
属性看名字猜测就是该对象的哈希值了,那么这个值是通过hashCode方法得到的吗?实际上,threadLocalHashCode
这个属性的得来非常的有趣,我们必须要去ThreadLocal源码中去看看!
public class ThreadLocal<T> {
/**
* 下一个hashCode
* 注意:这是个静态属性,那么只有在ThreadLocal的类第一次被加载进行类初始化的时候会被初始化,明显,初始化时为0。
*/
private static AtomicInteger nextHashCode = new AtomicInteger();
/**
* threadlocal对象的hashcode,并非通过HashCode方法得到,他有自己的计算规则
* 可以看到,它是调用nextHashCode()方法的返回值得来的
*/
private final int threadLocalHashCode = nextHashCode();
/**
* 每个threadLocal对象通过该方法获取自己的hashcode
*/
private static int nextHashCode() {
//内部使用nextHashCode对象的getAndAdd方法
//该方法首先返回当前的值,然后使得当前值的值加上指定的值,这里是HASH_INCREMENT
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
/**
* 哈希增量,顾名思义,就是哈希值的增量
*/
private static final int HASH_INCREMENT = 0x61c88647;
}
结合上面的几个属性和方法,我们终于明白:
在第一次创建ThreadLocal实例时,会加载ThreadLocal类,此时nextHashCode初始化值为0,然后是该对象threadLocalHashCode属性的初始化,在创建该类对象完毕之后,会自动调用nextHashCode方法,将此时nextHashCode的值作为自己的hashCode并且nextHashCode对象的值增加HASH_INCREMENT,明显是作为下一个ThreadLocal实例的hashCode值。
即,每一个ThreadLocal实例使用创建该实例时的nextHashCode值作为自己的hashCode,然后将nextHashCode值增加HASH_INCREMENT,作为下一个ThreadLocal实例的hashCode。
0x61c88647
是十六进制的数,转换为十进制就是1640531527,实际上这个哈希增量的值的选取和斐波那契散列法、黄金比例有关
2.4 get方法
对于不同的线程,每次获取变量值时,是从本线程内部的threadLocals中获取的,别的线程并不能获取到当前线程的值,形成了变量的隔离,互不干扰。大概步骤如下:
- 获取当前线程的成员变量threadLocals
- 如果threadLocals非空,调用getEntry方法尝试查找并返回节点e:
- 如果e不为null,说明找到了,那爱么返回e的value,方法结束
- 如果e为null,说明没找到,方法继续。
到这一步,说明可能是threadLocals为空,或者没找到e。那么调用setInitialValue方法,以当前ThreadLocal对象为key设置一个entry,并返回value。
/**
* ThreadLocal中的get方法,开放给外部调用
*
* @return 当前线程的当前ThreadLocal对象存入的值
*/
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的threadLocals对象
ThreadLocalMap map = getMap(t);
//如果map不为null,即表示已经初始化过
if (map != null) {
//从map获取对应的Entry节点,传入this代表当前的ThreadLocal对象
ThreadLocalMap.Entry e = map.getEntry(this);
//如果e不为null
if (e != null) {
//获取并返回值
T result = (T) e.value;
return result;
}
}
//否则,如果map为null,或者e为null
//那么返回null或者自定义的初始值
return setInitialValue();
}
2.4.1 getEntry方法
ThreadLocalMap内部的方法,根据key,尝试获取对应的Entry节点。 大概步骤如下:
- 根据key计算出桶位;
- 获取该桶位节点e,如果e不为null并且e的key和指定key相等(使用==比较),那么返回e,方法结束;
- 否则,调用getEntryAfterMiss方法进行一个步长的线性探测查找,查找过程中每碰到无效的节点,调用expungeStaleEntry进行清理;如果找到了则返回找到的entry;没有找到(探测到了空的桶位),则返回null。
/**
* ThreadLocalMap内部的方法,根据key,获取对应的Entry节点
*
* @param key key
* @return Entry节点,没找到就返回null
*/
private Entry getEntry(ThreadLocal<?> key) {
//根据key计算桶位
int i = key.threadLocalHashCode & (table.length - 1);
//获取Entry节点e
Entry e = table[i];
/*如果e不为nul并且并且e内部key等于当前key(ThreadLocal对象)*/
//可以看到key相等是使用==直接比较的
if (e != null && e.get() == key)
//则返回e
return e;
else
/*否则使用线性探测查找
线性探测查找过程中每碰到无效slot,调用expungeStaleEntry进行清理;如果找到了则返回entry;没有找到,返回null*/
return getEntryAfterMiss(key, i, e);
}
2.4.2 setInitialValue方法
ThreadLocal的方法,用于设置并返回初始值,在get方法没有找key对应的节点时,会调用该方法!
大概有如下几步:
- 获取initialValue方法的返回值,作为新节点的value;
- 获取当前线程的ThreadLocalMap,判断是否为null;
- 如果不为null,则以当前ThreadLocal对象为key,存放value,方法结束;
- 如果为null,则初始化此线程的ThreadLocalMap,并以当前ThreadLocal对象为key,存放value,方法结束。
/**
* ThreadLocal的方法,设置并返回初始值
*
* @return 返回null或者通过initialValue方法用户自定义的初始值
*/
private T setInitialValue() {
//返回null或者用户重写该方法时自定义的返回值
T value = initialValue();
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的map
ThreadLocal.ThreadLocalMap map = getMap(t);
//如果map不为null
if (map != null)
//尝试添加节点,以当前ThreadLocal对象为key,以null或者自定义的初始值为value
map.set(this, value);
else
//否则初始化map并设置值
createMap(t, value);
//返回value,null或者自定义的初始值
return value;
}
2.4.2.1 initialValue方法
当get方法没有找到数据时,会调用setInitialValue方法,该方法中会调用initialValue方法,将默认返回null,用户也可以重写该方法,用于返回指定的值,相当于默认初始值。
setInitialValue将会以当前ThreadLocal对象为key,initialValue的返回值为value,存放一个节点,同时返回value的值。
/**
* ThreadLocal的方法,默认返回空
*
* @return 默认返回null, 用户可以重写该方法返回自定义的初始值
*/
protected T initialValue() {
return null;
}
2.4.2.2 默认初始值案例
public class ThreadLocalInitialValue {
public static void main(String[] args) {
//th1覆写了initialValue方法
ThreadLocal th1 = new ThreadLocal() {
@Override
protected Object initialValue() {
return 11;
}
};
//th2没有覆写了initialValue方法
ThreadLocal th2 = new ThreadLocal();
//由于并没有调用set方法设置数据,那么两个ThreadLocal的get方法都将不能找到存放的数据
//此时th1将返回默认初始值,并设置key:th1 value:11
System.out.println("th1初始值:" + th1.get());
//th2将返回null,并设置key:th2 value:null
System.out.println("th2初始值:" + th2.get());
}
}
2.5 ThreadLocal的内存泄露
2.5.1 内存泄漏的原理
首先是基础知识,关于Java中的引用的介绍:Java中强、软、弱、虚四种对象引用的详解和案例演示。
根据上面的源码,我们知道在存放新结点时在Entry结点的构造器中,并不是直接使用ThreadLocal对象作为key的,而是使用由ThreadLocal对象包装成的弱引用对象作为Key的,key被弱引用的字段关联,获取key是也是从弱引用字段中获取的。
为什么使用弱引用包装的ThreadLocal对象作为key? 因为如果某个entry直接使使用一个普通属性和ThreadLocal对象关联,即key是强引用。那么当最外面ThreadLocal对象的全局变量引用置空时,由于在ThreadLocalMap中存在key对这个ThreadLocal对象的强引用,那么这个ThreadLocal对象并不会被回收,但此时我们已经无法访问、利用这个对象,造成了key的内存泄漏。
因此,ThreadLocal对象被包装为弱引用作为key。这样,当外部的ThreadLocal对象的强引用被清除时,由于在ThreadLocalMap中存储的是弱引用key,这个ThreadLocal对象只被弱引用对相关联,因此它就是一个弱引用对象,那么下一次GC时这个弱引用ThreadLocal对象可以自动被清除了。
但是,此时仍然会造成内存泄漏,不过此时是value或者说Entry的内存泄漏。
我们知道value是强引用。这就导致了一个问题,如果这个弱引用key被回收而变成null时,如果之前调用ThreadLocal方法设置值的线程一直持续运行,那么它的ThreadLocalMap也一直存在,那么内部的entry结点也一直存在,那么value肯定还存在,但是此时却不能通过key访问到了(因为key被回收变成null了),此时还是发生了内存泄露。
所以说,最保险的办法是移除无效的Entry。
2.5.2 如何避免内存泄漏
我们在set和get方法的源码中能够看到,当遍历的entry的key为null时,此时将清除该entry,value置空,这样就可以解决部分内存泄漏问题。但这并不是绝对的,可能并没有遍历到key为null的entry时set、get方法就因为插入、获取成功而返回了,因此在set、get方法中,只会尝试将遍历的到无效数据清除,并且这种方式是一种被动的清除,不能即时清除无效数据。
ThreadLocal还有一个remove方法,该方法可以将此ThreadLocal对象对应的entry清除。实际上,在对ThreadLocal的数据使用完毕之后,从逻辑上来说此时的entry就是无效的数据了,因此主动调用一次remove方法,将该entry移除。这样我们对使用完毕的entry进行手动清除,从根本上杜绝了内存泄漏问题。
所以养成良好的编程习惯十分重要,使用完ThreadLocal的数据之后,一定要记得调用一次remove方法。
3 总结与应用
总结:
- 每个ThreadLocal由于实现线程本地存储,但是只能保存一个本地数据,如果想要一个线程能够保存多个数据,就需要创建多个ThreadLocal。
- ThreadLocalMap的key键为ThreadLocal包装成的弱引用,value为设置的值。ThreadLocal会有内存泄漏的风险,因此使用完毕必须手动调用remove清除!
应用:
- 使用ThreadLocal的典型场景正如上面的数据库连接管理,线程会话管理等场景,只适用于独立变量副本的情况,如果变量为全局共享的,则不适用在高并发下使用。
- Spring MVC对于每一个请求线程的Request对象使用ThreadLocal属性封装到RequestContextHolder中,这样每条线程都能访问到自己的Request。
- Spring 声明式事务管理中,每一个事务的信息也是使用ThreadLocal属性封装到TransactionSynchronizationManager中的,以此实现不同线程的事务存储和隔离,以及事务的挂起和恢复。
- 原始的JDBC方式的时候可以使用ThreadLocal类来管理事务!
作者:刘Java
链接:https://juejin.cn/post/7021355637812494366
来源:稀土掘金