JDK ThreadLocal
如果你需要变量在多线程之间隔离,或者在同线程内的类和方法中共享,那么 ThreadLocal 大显身手的时候就到了。ThreadLocal 可以理解为线程本地变量,它是 Java 并发编程中非常重要的一个类。
ThreadLocal 为变量在每个线程中都创建了一个副本,该副本只能被当前线程访问,多线程之间是隔离的,变量不能在多线程之间共享。这样每个线程修改变量副本时,不会对其他线程产生影响。
JDK ThreadLocal类
在上面谈到了对ThreadLocal的一些理解,那我们下面来看一下具体ThreadLocal是如何实现的。先了解一下ThreadLocal类提供的几个方法:
java
public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }
首先我们来看一下ThreadLocal类是如何为每个线程创建一个变量的副本的。
JDK ThreadLocal:get
先看下get方法的实现:
java
public T get() {
Thread t = Thread.currentThread();
//使用当前线程作为key 获取threadLocals
//threadLocals 是 一个ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//使用自己作为key 从 ThreadLocalMap中获取value
//注意这里获取键值对传进去的是this,即ThreadLocal 而不是当前线程t。
ThreadLocalMap.Entry e = map.getEntry(this);
//如果获取成功,则返回value值。
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//该方法中调用了子类重写的方法是模板模式
//如果map为空,则调用setInitialValue方法返回value。
return setInitialValue();
}
//返回的是当前线程的成员变量 threadLocals
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
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
//找到的是null
return getEntryAfterMiss(key, i, e);
}
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;
}
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;
}
void createMap(Thread t, T firstValue) {
//this是ThreadLocal
//value是initialValue();
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
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);
}
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
JDK ThreadLocal:set
java
public void set(T value) {
Thread t = Thread.currentThread();
//获取ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//获取下标
int i = key.threadLocalHashCode & (len-1);
//Entry e = tab[i] 头元素
//e = tab[i = nextIndex(i, len)] 获取下一个元素
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//如果找到相同的key 说明是覆盖更新 设置值后直接返回
if (k == key) {
e.value = value;
return;
}
//如果为null 说明原来的被回收
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
JDK ThreadLocal原理
接下来我们看看 ThreadLocal 的实现原理。既然多线程访问 ThreadLocal 变量时都会有自己独立的实例副本,那么很容易想到的方案就是在 ThreadLocal 中维护一个 Map,记录线程与实例之间的映射关系。当新增线程和销毁线程时都需要更新 Map 中的映射关系,因为会存在多线程并发修改,所以需要保证 Map 是线程安全的。那么 JDK 的 ThreadLocal 是这么实现的吗?
答案是 NO。
因为在高并发的场景并发修改 Map 需要加锁,势必会降低性能。JDK 为了避免加锁,采用了相反的设计思路。以 Thread 入手,在 Thread 中维护一个 Map,记录 ThreadLocal 与实例之间的映射关系,这样在同一个线程内,Map 就不需要加锁了。
为了更加深入理解 ThreadLocal,了解 ThreadLocalMap 的内部实现是非常有必要的。ThreadLocalMap 其实与 HashMap 的数据结构类似,但是 ThreadLocalMap 不具备通用性,它是为 ThreadLocal 量身定制的。
ThreadLocalMap 是一种使用线性探测法实现的哈希表,底层采用数组存储数据。
如下图所示,ThreadLocalMap 会初始化一个长度为 16 的 Entry 数组,每个 Entry 对象用于保存 key-value 键值对。与 HashMap 不同的是,Entry 的 key 就是 ThreadLocal 对象本身,value 就是用户具体需要存储的值。
用数组是因为,我们开发过程中可以一个线程可以有多个TreadLocal来存放不同类型的对象的,但是他们都将放到你当前线程的ThreadLocalMap里,所以肯定要数组来存。
ThreadLocal示例
java
public class ThreadLocalTest {
private static final ThreadLocal<String> THREAD_NAME_LOCAL = ThreadLocal.withInitial(() -> Thread.currentThread().getName());
private static final ThreadLocal<TradeOrder> TRADE_THREAD_LOCAL = new ThreadLocal<>();
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
int tradeId = i;
new Thread(() -> {
TradeOrder tradeOrder
= new TradeOrder(tradeId, tradeId % 2 == 0 ? "已支付" : "未支付");
TRADE_THREAD_LOCAL.set(tradeOrder);
System.out.println("threadName: " + THREAD_NAME_LOCAL.get());
System.out.println("tradeOrder info:" + TRADE_THREAD_LOCAL.get());
}, "thread-" + i).start();
}
}
static class TradeOrder {
long id;
String status;
public TradeOrder(int id, String status) {
this.id = id;
this.status = status;
}
@Override
public String toString() {
return "id=" + id + ", status=" + status;
}
}
}
在上述示例中,构造了 THREAD_NAME_LOCAL 和 TRADE_THREAD_LOCAL 两个 ThreadLocal 变量,分别用于记录当前线程名称和订单交易信息。ThreadLocal 是可以支持泛型的,THREAD_NAME_LOCAL 和 TRADE_THREAD_LOCAL 存放 String 类型和 TradeOrder 对象类型的数据,你可以通过 set()/get() 方法设置和读取 ThreadLocal 实例。一起看下示例代码的运行结果:
java
threadName: thread-0
threadName: thread-1
tradeOrder info:id=1, status=未支付
tradeOrder info:id=0, status=已支付
可以看出 thread-1 和 thread-2 虽然操作的是同一个 ThreadLocal 对象,但是它们取到了不同的线程名称和订单交易信息。
ThreadLocalMap 是一种使用线性探测法实现的哈希表,底层采用数组存储数据。
ThreadLocalMap 会初始化一个长度为 16 的 Entry 数组,每个 Entry 对象用于保存 key-value 键值对。
与 HashMap 不同的是,Entry 的 key 就是 ThreadLocal 对象本身,value 就是用户具体需要存储的值。
当调用 ThreadLocal.set() 添加 Entry 对象时,是如何解决 Hash 冲突的呢?这就需要我们了解线性探测法的实现原理。
JDK ThreadLocal:Hash冲突:线性探测
当调用 ThreadLocal.set() 添加 Entry 对象时,是如何解决 Hash 冲突的呢?
java
//即每次调用new ThreadLocal 都会调用这个方法
private final int threadLocalHashCode = nextHashCode();
private static final int HASH_INCREMENT = 0x61c88647;
private static AtomicInteger nextHashCode =new AtomicInteger();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//赋值给nextHashCode
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
这就需要我们了解线性探测法的实现原理。每个 ThreadLocal 在初始化时都会有一个 Hash 值为 threadLocalHashCode,每增加一个 ThreadLocal, Hash 值就会固定增加一个魔术 HASH_INCREMENT = 0x61c88647。
为什么取 0x61c88647 这个魔数呢?
实验证明,通过 0x61c88647 累加生成的 threadLocalHashCode 与 2 的幂取模,得到的结果可以较为均匀地分布在长度为 2 的幂大小的数组中。有了 threadLocalHashCode 的基础,下面我们通过下面的表格来具体讲解线性探测法是如何实现的。
为了便于理解,我们采用一组简单的数据模拟 ThreadLocal.set() 的过程是如何解决 Hash 冲突的。
threadLocalHashCode = 4,threadLocalHashCode & 15 = 4;此时数据应该放在数组下标为 4 的位置。下标 4 的位置正好没有数据,可以存放。
threadLocalHashCode = 19,threadLocalHashCode & 15 = 4;但是下标 4 的位置已经有数据了,如果当前需要添加的 Entry 与下标 4 位置已存在的 Entry 两者的 key 相同,那么该位置 Entry 的 value 将被覆盖为新的值。我们假设 key 都是不相同的,所以此时需要向后移动一位,下标 5 的位置没有冲突,可以存放。
threadLocalHashCode = 33,threadLocalHashCode & 15 = 3;下标 3 的位置已经有数据,向后移一位,下标 4 位置还是有数据,继续向后查找发现5也有数据,发现下标 6 没有数据,可以存放。
ThreadLocal.get() 的过程也是类似的,也是根据 threadLocalHashCode 的值定位到数组下标,然后判断当前位置 Entry 对象与待查询 Entry 对象的 key 是否相同,如果不同,继续向下查找。
由此可见,ThreadLocal.set()/get() 方法在数据密集时很容易出现 Hash 冲突,需要 O(n) 时间复杂度解决冲突问题,效率较低。
弱引用
java
package WeakReference;
import java.lang.ref.WeakReference;
/***
* 学校类 继承了WeakReference,引用对象是Teacher
* 指的是学校对老师的引用是1个弱引用
***/
public class School extends WeakReference<Teacher> {
String name = "清华大学";
public School(Teacher referent) {
super(referent);
}
public School(Teacher referent,String name) {
super(referent);
this.name = name;
}
}
java
package WeakReference;
/***
* 教师类
*/
public class Teacher {
String name;
Teacher() {
}
Teacher(String name) {
this.name = name;
}
}
csharp
package WeakReference;
public class Test {
public static void main(String[] args) {
ThreadLocal threadLocal = new ThreadLocal();
threadLocal.set(1);
//张三老师是强引用 他被当前main类引用
Teacher teacher = new Teacher("张三");
//李四老师是弱引用
School school = new School(new Teacher("李四"));
//在一个对象只有弱引用的时候 会被gc掉
System.gc();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (threadLocal!=null)
System.out.println("--------------------" + threadLocal.get());
System.out.println("学校还在吗?" + school.name);
//下课后我们再看下 李四老师是不是还存在
if (school.get() == null) {
System.err.println("李四老师被弱引用学校引用已经被回收!!!");
} else {
System.out.println("李四老师还在!!!");
}
//我们要理解弱引用对象 和被弱引用对象
//弱引用对象就是我们的学校 gc过后只有被弱引用对象 也就是teacher被回收了 school没有被回收
//很多同学对这个位置的理解不对
System.out.println("-----------------学校名称" + school.name);
//使用弱引用指向张三
school = new School(teacher);
System.gc();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//下课后我们再看下 张三老师是不是还存在
if (school.get() == null) {
System.err.println("张三老师竟然被开除了!!!!");
} else {
System.out.println("张三老师还在,大家不必担心");
}
ThreadLocal local = new ThreadLocal();
local.set("当前线程名称:" + Thread.currentThread().getName());//将ThreadLocal作为key放入threadLocals.Entry中
Thread t = Thread.currentThread();//注意断点看此时的threadLocals.Entry数组刚设置的referent是指向Local的,referent就是Entry中的key只是被WeakReference包装了一下
local = null;//断开强引用,即断开local与referent的关联,但Entry中此时的referent还是指向Local的,为什么会这样,当引用传递设置为null时无法影响传递内的结果
System.gc();//执行GC
t = Thread.currentThread();//这时Entry中referent是null了,被GC掉了,因为Entry和key的关系是WeakReference,并且在没有其他强引用的情况下就被回收掉了
//如果这里不采用WeakReference,即使local=null,那么也不会回收Entry的key,因为Entry和key是强关联
//但是这里仅能做到回收key不能回收value,如果这个线程运行时间非常长,即使referent GC了,value持续不清空,就有内存溢出的风险
//彻底回收最好调用remove
//即:local.remove();//remove相当于把ThreadLocalMap里的这个元素干掉了,并没有把自己干掉
System.out.println(local);
}
}
JDK ThreadLocal:内存泄漏
强弱引用与内存泄露
关于强引用,大家都知道这么一段话:
强引用就是指在程序代码之中普遍存在的引用,如果一个对象具有强引用,那么JVM必定不会回收这个强引用的对象,即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象
那么就有一个问题,Object obj=new Object(),obj作为强引用存在虚拟机栈中,而new Object()作为对象存在于堆中,当obj的作用域结束,对应的虚拟机栈消失,obj引用也同时消失,但new Object()对象却仍然存在于堆中,"JVM必定不会回收这个对象" ,那jvm不是很容易就OOM了吗?
显然,我们可以手动设置obj=null,这样gc就会主动地将new Object()对象回收,而这样肯定是太繁琐了。
实际上,这句话缺少了一个前提,就是这个对象要和强引用还有关联,也就是在根搜索算法中,还和GC Roots相连接,这样jvm就一定不会回收强引用的对象。
上述的例子中,obj强引用消失后,new Object()和它的关联也就断了,这样就不再和GC Roots相连接,gc在之后的某个时间就会回收这个对象了
只具有弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
模拟ThreadLocal内存回收
根据上面的代码我们知道了ThreadLocal:Hash冲突:线性探测。
1.计算ThreadLocal的hash值:累加HASH_INCREMENT。
2.使用hash和数组长度取模计算下标。
3.构建1个Entry,Entry继承自WeakReference,包含ThreadLocal和value。其中只有ThreadLocal被弱引用引用。
java
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
4.根据下标将Entry加入数组。如果当前计算的下标在对应的位置已经有Entry并且key不是当前的ThreadLocal。
5.那么将下标+1继续往后找,直到找到1个空的位置为止。
下面模拟Entry数组强引用Entry,Entry弱引用ThreadLocal。
java
package com.dtyunxi.haier.center.account.bo.utils.A;
import java.lang.ref.WeakReference;
public class Entry extends WeakReference<Object> {
String value;
public Entry(Object referent,String value) {
super(referent);
this.value=value;
}
/**
最后发现entry.get()是空
entry不是空
原因:我们持有的是对entry的强引用
对于new ThreadLocal()来说它仍然是被弱引用引用
***/
public static void main(String args[]) {
/* ThreadLocal threadLocal = new ThreadLocal();
Entry entry = new Entry(threadLocal,"Hello");*/
Entry entry = new Entry(new ThreadLocal(),"Hello");
//把array看做是Entry数组 把Entry放到Entry数组
Object[] array = {entry};
if (entry.get() != null) {
System.out.println("not null before gc");
}
System.out.println(entry.value);
System.gc();
if (entry.get() != null) {
System.out.println("not null after gc");
} else {
System.out.println("null after gc");
}
System.out.println(entry.value);
System.out.println(entry == null);
}
}
//not null before gc
////null after gc
////false
ThreadLocal 内存泄漏的原因
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有线程退出以后,value的强引用链条才会断掉。
但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
java
栈上Thread -> 堆上Thread -> ThreaLocalMap -> Entry -> value
value永远无法回收,造成内存泄漏。
value永远无法回收,造成内存泄漏。
value永远无法回收,造成内存泄漏。
模拟一下
java
package com.dtyunxi.haier.center.account.bo.utils.A;
public class MockMemoryLeak {
ThreadLocal threadLocal;
public MockMemoryLeak(ThreadLocal threadLocal, String value) {
this.threadLocal = threadLocal;
threadLocal.set(value);
}
public static void main(String args[]) {
ThreadLocal threadLocal = new ThreadLocal();
MockMemoryLeak leak = new MockMemoryLeak(threadLocal, "Strong");
getThreadLocal();
System.gc();
//代码执行到这里发现getThreadLocal方法设置的ThreadLocal已经被回收了
//原因是getThreadLocal方法执行完毕,value=weak的ThreadLocal已经没有强引用了
//但是通过构造方法Entry2构造的实体没有被回收
//原因是 leak 虽然存在对value=strong的threadLocal的强引用
/****
threadLocals = {ThreadLocal$ThreadLocalMap@512}
table = {ThreadLocal$ThreadLocalMap$Entry[16]@515}
3 = {ThreadLocal$ThreadLocalMap$Entry@516}
value = "Strong"
referent = {ThreadLocal@496}
queue = {ReferenceQueue$Null@522}
next = null
discovered = null
10 = {ThreadLocal$ThreadLocalMap$Entry@519}
value = "weak"
referent = null
queue = {ReferenceQueue$Null@522}
next = {ThreadLocal$ThreadLocalMap$Entry@519}
discovered = {WeakHashMap$Entry@530} "null" ->
size = 2
threshold = 10
***/
Thread thread = Thread.currentThread();
}
private static ThreadLocal getThreadLocal() {
ThreadLocal threadLocal = new ThreadLocal();
threadLocal.set("weak");
return threadLocal;
}
}
为什么要设置为弱引用
下面我们再聊聊 ThreadLocalMap 中 Entry 的设计原理。
java
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
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();
}
Entry 继承自弱引用类 WeakReference,Entry是弱引用
在 JVM 垃圾回收时,只要发现了被弱引用引用的的对象,不管内存是否充足,都会被回收。
ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了。
那么为什么 Entry要设计成弱引用呢?
key 使用强引用
假如线程是在线程池,如果ThreadLocal被强引用,当某些ThreadLocal 不再使用时。
此时的引用关系如下:
java
栈上Thread 强引用 堆上Thread
堆上Thread 强引用ThreadLocalMap
ThreadLocalMap 强引用 Entry[] table;
Entry[] table 强引用 Entry。
Entry 强引用 ThreadLocal和value
如果线程一直存活:
如果Entry存在对ThreadLocal 的强引用,那么GC是无法回收的,从而造成内存泄漏。
当ThreadLocalMap的key为强引用。回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用.
这些Entry的key和value就会一直存在一条强引用链:
rust
栈上Thread -> 堆上Thread -> ThreaLocalMap -> Entry -> ThreadLocal和value
如果没有手动删除,ThreadLocal和value都不会被回收,导致Entry内存泄漏。
注意是ThreadLocal和value都不会被回收,导致Entry内存泄漏。
Key使用弱引用
如果Entry设置为弱引用,GC是可以回收ThreadLocal(参考案例1)。
前提是已不存在对ThreadLocal的强引用,即该线程的方法栈都出栈不存在对ThreadLocal的强引用。参考案例2。
ThreadLocal在没有其他的外部强引用时,发生GC时会被回收,比如上面的案例1。正常情况下代码这么写:
java
public class ThreadLocalTest {
public static void main(String[] args) {
ThreadLocal<TradeOrder> TRADE_THREAD_LOCAL = new ThreadLocal<>();
TradeOrder tradeOrder = new TradeOrder();
TRADE_THREAD_LOCAL.set(tradeOrder);
}
}
当线程运行完毕,TRADE_THREAD_LOCAL的强引用会在栈上被回收。
此时TRADE_THREAD_LOCAL只存在Entry对它的弱引用,此时TRADE_THREAD_LOCAL是安全的,不存在内存泄露。
但是Entry的value就会一直存在一条强引用链:
java
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
如果没有手动删除,ThreadLocal和value都不会被回收,导致value内存泄漏。
但是如果ThreadLocal的关联的线程一直持续运行,也就是Thread一直引用这个ThreadLocal。
那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
注意是Entry对象中的value就有可能一直得不到回收,发生内存泄露
注意是Entry对象中的value就有可能一直得不到回收,发生内存泄露
注意是Entry对象中的value就有可能一直得不到回收,发生内存泄露
比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。
按照道理一个线程使用完,ThreadLocalMap是应该要被清空的,但是现在线程被复用了。
如果ThreadLocal是弱引用,那么在ThreadLocal没有被强引用时,线程执行完毕。
虽然Entry设计成了弱引用,但是当发生GC,ThreadLocal GC被回收后,ThreadLocalMap 中可能出现 Entry的key为 NULL,那么Entry的value一直会强引用数据而得不到释放,只能等待线程销毁,此时仍然存在内存泄露。
如何避免ThreadLocal内存泄漏呢?
ThreadLocal 已经帮助我们做了一定的保护措施,在执行 ThreadLocal.set()/get() 方法时。
ThreadLocal 会清除 ThreadLocalMap 中 key 为 NULL 的 Entry 对象,让它还能够被 GC 回收。
试想以下场景:
1个线程在线程池一直存活,当它的方法出栈它的方法栈上的ThreadLocal就会被回收,此时ThreadLocal只有Entry对它
的弱引用。此时GC会回收ThreadLocal,那么ThreadLocalMap中就会存在key 为 NULL 的 Entry 对象。
java
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
//找到的是null
return getEntryAfterMiss(key, i, e);
}
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;
}
除此之外,当线程中某个 ThreadLocal 对象不再使用时,立即调用 remove() 方法删除 Entry 对象。如果是在异常的场景中,记得在 finally 代码块中进行清理,保持良好的编码意识。
主动调用remove方法,及时清理。
目前我们使用多线程都是通过线程池管理的,对于核心线程数之内的线程都是长期驻留池内的。显式调用remove,一方面是防止内存泄漏,最为重要的是,不及时清除有可能导致严重的业务逻辑问题,产生线上故障(使用了上次未清除的值)。
csharp
JVM利用设置ThreadLocalMap的Key为弱引用,来避免内存泄露。
JVM利用调用remove、get、set方法的时候,回收弱引用。
当ThreadLocal存储很多Key为null的Entry的时候,而不再去调用remove、get、set方法,那么将导致内存泄漏。
当使用static ThreadLocal的时候,延长ThreadLocal的生命周期,那也可能导致内存泄漏。因为,static变量在类未加载的时候,它就已经加载,当线程结束的时候,static变量不一定会回收。那么,比起普通成员变量使用的时候才加载,static的生命周期加长将更容易导致内存泄漏危机.
JDK ThreadLocal案例
java
package io.netty.example.chapter1.echo;
public class Test {
ThreadLocal<Long> longLocal = new ThreadLocal<Long>() {
@Override
protected Long initialValue() {
return 0L;
}
};
ThreadLocal<String> stringLocal = new ThreadLocal<String>();
public void set(long value) {
longLocal.set(value);
stringLocal.set(value +"");
}
public void get() {
System.out.println("--------------------" + Thread.currentThread().getName());
System.out.println(this.getLong());
System.out.println(this.getString());
}
public long getLong() {
return longLocal.get();
}
public String getString() {
return stringLocal.get();
}
public static void main(String[] args) throws InterruptedException {
final Test test = new Test();
// 因为long所在的threadLocal 重写了initValue方法
// 所以可以get到值 get到的值是initValue方法返回的值
test.get();
test.set(1);
test.get();
Thread thread1 = new Thread() {
@Override
public void run() {
test.set(2);
test.get();
}
};
thread1.start();
thread1.join();
test.set(3);
test.get();
}
}
这段代码的输出结果为:
java
--------------------main
0
null
--------------------main
1
1
--------------------Thread-0
2
2
--------------------main
3
3
从这段代码的输出结果可以看出,在main线程中和thread1线程中,longLocal保存的副本值和stringLocal保存的副本值都不一样。最后一次在main线程再次打印副本值是为了证明在main线程中和thread1线程中的副本值确实是不同的。
总结一下:
1)实际的通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;
2)为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量,就像上面代码中的longLocal和stringLocal;
3)在进行get之前,必须先set,否则会报空指针异常;
如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。
因为在上面的代码分析过程中,我们发现如果没有先set的话,即在map中查找不到对应的存储,则会通过调用setInitialValue方法返回i,而在setInitialValue方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null。
Spring事务:Threadlocal
Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。
Spring的事务主要是ThreadLocal和AOP去做实现的,我这里提一下,大家知道每个线程的链接是靠ThreadLocal保存的就好了。
共享线程ThreadLocal数据
InheritableThreadLocal
使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。