在《码处高效:Java开发手册》这本书上详细描述了ThreadLocal的原理,也有过度设计的说法,
难道弱引用设计真的没必要吗?对此老吕要仔细分析分析,ThreadLocal到底该不该使用弱引用设计。
JDK作为构建Java应用生态的原始生产资料和工具,它的每一行代码相信都是经过深思熟虑的(也有考虑不周的,后续版本会标上废弃或者改进代码)。
回忆ThreadLocal原理
ThreadLocal本质是一个访问Thread上ThreadLocalMap对象的操作工具类;
为什么业务代码不能直接访问Thread获取ThreadLocalMap来操作呢?
1、因为类ThreadLocalMap是定义在ThreadLocal的一个内部类,访问权限是protected级别,这就是说你的应用代码是无法直接定义使用ThreadLocalMap类的。
2、实例变量 ThreadLocalMap在Thread中定义的访问权限也是protected级别,你的业务代码也是无法直接访问的。
所以你只能通过ThreadLocal去访问Thread上的ThreadLocalMap对象。
(有通过反射想法去访问Thread上ThreadLocalMap对象并操作Map的同学吗?可以去试一把)
在Entry对象中key是指向ThreadLocal对象的弱引用,Value是指向值对象的强引用;
所以ThreadLocal机制的核心技术是在ThreadLocalMap类上。
回忆ThreadLocalMap内存释放原理
前面文章中也讲清楚了 ThreadLocalMap内存释放原理和触发场景
最理想的情况是 主动调用 remove方法来释放不再使用的Map条目
在内存泄漏、哈希冲突、扩容等情况下 ThreadLocalMap会顺便清理失效的条目,释放内存,从而将有效条目放到更优的位置
强引用设计的后果
直接后果就是 1、ThreadLocal 对象内存泄漏;2、ThreadLocalMap的失效条目没有任何被回收的机会,直到线程结束;
内存泄漏的直接原因是 线程上下文在释放当前线程对应ThreadLocalMap对应Entry之前 释放了 ThreadLocal对象的强引用,导致业务代码不可能再访问到那个Entry,但是Entry内存又无法被GC,从而形成内存泄漏。
弱引用设计的好处
1、至少ThreadLocal对象可以被GC;
2、在线程结束之前在某些场景下 Entry有被回收的机会;
ThreadLocalMap与HashMap的对比
ThreadLocalMap也是一个map但没有实现Map接口,只是一个实现了主要接口(get\set\remove)的为ThreadLocal量身定制的Map.
key引用设计 |
能否回收失效条目 |
|
HashMap |
强引用 |
不能,原因:不能判断哪些条目已失效 |
ThreadLocalMap |
弱引用 |
能,原因:能通过判断key=null发现失效的条目,从而进行回收。弱引用的设计使得ThreadLocal对象有机会被GC,从而导致key=null |
通过实验测试失效条目的回收
触发失效条目回收是有条件的,本实验只触发了部分场景的回收,只是为了说明它确实能回收失效条目,弱引用的设计是有很大意义的。
先说下影响自动回收的因素:
1、只有弱引用对象被GC后,才会产生失效条目(key=null),才有触发失效条目回收的可能;
2、弱引用对象被GC之前,ThreadLocalMap的回收机制根本无法判断出失效条目,这时和强引用是没有区别的;
3、在弱引用对象被GC后,产生失效条目,在下一次get、set、remove时都有机会触发回收,但是并不一定能触发;
4、在hash冲突出现的情况下,触发回收的机会会大大提升;
5、在没有set的情况下直接get会触发条目初始化操作,这时相当于一个set操作,也有机会触发回收;
6、在没有set的情况下直接remove不一定会触发回收,因为不一定发生hash冲突;
公共代码:
/**
* @Project fighting-core
* @Description 用来填充 value
* @Author lvaolin
* @Date 2023/4/1 上午11:43
*/
public class MyBigObject {
private String bigString;
public MyBigObject(String bigString){
this.bigString = bigString;
}
}
/**
* @Project fighting-core
* @Description 反射工具
* @Author lvaolin
* @Date 2023/4/2 下午6:40
*/
public class MyReflectUtil {
/**
* 打印当前线程上关联的ThreadLocalMap容量和条目数量(包括有效和无效条目)
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
public static void printThreadLocalMapSize() throws NoSuchFieldException, IllegalAccessException {
Thread thread = Thread.currentThread();
Field threadLocalsField = Thread.class.getDeclaredField("threadLocals");
threadLocalsField.setAccessible(true);
Object threadLocals = threadLocalsField.get(thread);
Field tableField = threadLocals.getClass().getDeclaredField("table");
tableField.setAccessible(true);
Object entryArr = tableField.get(threadLocals);
int length = Array.getLength(entryArr);//threadLocalMap length
Field sizeField = threadLocals.getClass().getDeclaredField("size");
sizeField.setAccessible(true);
Object size = sizeField.get(threadLocals);//threadLocalMap size
System.out.println("threadLocalMap:length->"+length+",size->"+size);
}
}
场景验证1:弱引用对象被GC之前,是不会触发自动回收的
/**
* @Project fighting-core
* @Description threadLocalMap内存回收验证
* @Author lvaolin
* @Date 2023/4/1 上午11:41
*/
public class MyService1 {
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
MyService1 myService = new MyService1();
myService.m1();
}
public void m1() throws IOException, NoSuchFieldException, IllegalAccessException {
System.out.println("map初始状态:");
MyReflectUtil.printThreadLocalMapSize();
for (int i = 1; i <= 100; i++) {
ThreadLocal tl = new ThreadLocal();
tl.set(new MyBigObject("set big object"+tl.hashCode()));
}
System.out.println("map存储100个条目后:");
MyReflectUtil.printThreadLocalMapSize();
}
}
map初始状态:
threadLocalMap:length->16,size->3
map存储100个条目后:
threadLocalMap:length->256,size->103
场景验证2:弱引用对象被GC之后,是会触发自动回收的
public class MyService2 {
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
MyService2 myService = new MyService2();
myService.m1();
}
public void m1() throws IOException, NoSuchFieldException, IllegalAccessException {
System.out.println("map初始状态:");
MyReflectUtil.printThreadLocalMapSize();
for (int i = 1; i <= 100; i++) {
ThreadLocal tl = new ThreadLocal();
tl.set(new MyBigObject("set big object"+tl.hashCode()));
//x越小,gc越频繁
// threadlocal弱引用对象回收越及时,
// threadlocalMap回收越及时(前面文章讲了什么情况下触发回收)
int x = 1;
if (i%x==0)System.gc();
}
System.out.println("map存储100个条目后:");
MyReflectUtil.printThreadLocalMapSize();
}
x=1情况下:
map初始状态:
threadLocalMap:length->16,size->3
map存储100个条目后:
threadLocalMap:length->16,size->5
x=20情况下:
map初始状态:
threadLocalMap:length->16,size->3
map存储100个条目后:
threadLocalMap:length->64,size->24
x=100情况下:
map初始状态:
threadLocalMap:length->16,size->3
map存储100个条目后:
threadLocalMap:length->256,size->103
场景验证3:弱引用对象被GC之后,get操作触发回收
public class MyService3 {
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
MyService3 myService = new MyService3();
myService.m1();
}
public void m1() throws IOException, NoSuchFieldException, IllegalAccessException {
System.out.println("map初始状态:");
MyReflectUtil.printThreadLocalMapSize();
for (int i = 1; i <= 100; i++) {
ThreadLocal tl = new ThreadLocal();
tl.set(new MyBigObject("set big object"+tl.hashCode()));
}
System.out.println("map存储100个条目后:");
MyReflectUtil.printThreadLocalMapSize();
System.gc();
ThreadLocal tl = new ThreadLocal();
//get时如果没有找到会进行条目初始化,默认key=threadLocal,value=null
tl.get();
//tl.set(new MyBigObject("set big object"+tl.hashCode()));
//tl.remove();
System.out.println("GC后进行get操作后:");
MyReflectUtil.printThreadLocalMapSize();
}
}
map初始状态:
threadLocalMap:length->16,size->3
map存储100个条目后:
threadLocalMap:length->256,size->103
GC后进行get操作后:
threadLocalMap:length->256,size->4
场景验证4:弱引用对象被GC之后,set操作触发回收
public class MyService4 {
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
MyService4 myService = new MyService4();
myService.m1();
}
public void m1() throws IOException, NoSuchFieldException, IllegalAccessException {
System.out.println("map初始状态:");
MyReflectUtil.printThreadLocalMapSize();
for (int i = 1; i <= 100; i++) {
ThreadLocal tl = new ThreadLocal();
tl.set(new MyBigObject("set big object"+tl.hashCode()));
}
System.out.println("map存储100个条目后:");
MyReflectUtil.printThreadLocalMapSize();
System.gc();
ThreadLocal tl = new ThreadLocal();
//tl.get();
tl.set(new MyBigObject("set big object"+tl.hashCode()));
//tl.remove();
System.out.println("GC后进行set操作后:");
MyReflectUtil.printThreadLocalMapSize();
}
}
map初始状态:
threadLocalMap:length->16,size->3
map存储100个条目后:
threadLocalMap:length->256,size->103
GC后进行set操作后:
threadLocalMap:length->256,size->4
场景验证5:弱引用对象被GC之后,remove操作
public class MyService5 {
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
MyService5 myService = new MyService5();
myService.m1();
}
public void m1() throws IOException, NoSuchFieldException, IllegalAccessException {
System.out.println("map初始状态:");
MyReflectUtil.printThreadLocalMapSize();
for (int i = 1; i <= 100; i++) {
ThreadLocal tl = new ThreadLocal();
tl.set(new MyBigObject("set big object"+tl.hashCode()));
}
System.out.println("map存储100个条目后:");
MyReflectUtil.printThreadLocalMapSize();
System.gc();
ThreadLocal tl = new ThreadLocal();
//tl.get();
//tl.set(new MyBigObject("set big object"+tl.hashCode()));
tl.remove();
System.out.println("GC后进行remove操作后:");
MyReflectUtil.printThreadLocalMapSize();
}
}
map初始状态:threadLocalMap:length->16,size->3
map存储100个条目后:threadLocalMap:length->256,size->103
GC后进行remove操作后:threadLocalMap:length->256,size->103
因为没有set就直接进行了remove,remove定位元素时也没有发生hash冲突,所以就没有触发任何回收场景;
总结
1、ThreadLocal机制的弱引用设计是有意义的,它能在正常操作中尽力自动回收泄漏的内存,避免hash扩容,降低内存占用;
2、如果没有弱引用设计,那么ThreadLocalMap将和HashMap一样不能自动回收任何内存;
3、ThreadLocalMap是专门为ThreadLocal机制定制的,只实现了基本get、set、remove操作,特色是使用了线性探测法来解决hash冲突问题,
同时map key 弱引用的设计为自动回收泄漏内存奠定了基础;