ThreadLocal
1、ThreadLocal的使用场景
ThreadLocal在Java的开发中非常常见,一般在以下情况中会使用到ThreadLocal。
- 1、在进行对象跨层传递的时候,可以考虑使用ThreadLocal,避免方法多次传递,打破层次间的约束。例如SpringMVC中的Controller,Service,Dao 3层中,同一个线程只对应一个用户ID,用户名字等,该副本只供该线程所用
- 2、线程间数据隔离
- 3、进行事务操作,用于存储线程事务信息
2、概述
我们知道在多线程的情况下访问一个共享资源如果不上锁的话势必会被不同的线程所修改,造成数据读取出来的值可能并非如预期, 而ThreadLocal是一种不需要上锁也能保证线程之间数据同步的问题的主件,简单来说,ThreadLocal采取的是一种空间换时间的策略,每个线程有独立的数据副本,比如说有5个线程,则5个副本就存在于线程的上下文当中, 使得每一个线程都有一个变量独立的副本,而这个副本无论如何也不可能被其它的线程所访问到,也就不可能被其它线程所修改,显然就达到了上锁的效果,达到线程变量的一种隔离。
ThreadLocal类的官方文档
image.png3、简单使用
package com.concurrency2;
public class MyTest8 {
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal<String>();
threadLocal.set("hello");
threadLocal.set("world");
new Thread(() -> {
threadLocal.set("thread1");
System.out.println("thread1: " + threadLocal.get());
}).start();
new Thread(() -> {
threadLocal.set("thread2");
System.out.println("thread2: " + threadLocal.get());
}).start();
System.out.println(threadLocal.get());
}
}
输出
world
thread2: thread2
thread1: thread1
分析:
从输出可以知道,虽然使用同一个ThreadLocal对象,但是不同线程之间数据不会有错乱的现象,各个线程之间都有独立的副本
4、Thread,ThreadLocal,ThreadLocalMap的关系图
image.pngimage.png
简单说明set,get,remove方法
1、set方法:将当前线程的此线程局部变量的副本设置为指定的值。然后再清除对应的key为null的value。
2、get方法:返回当前线程的此线程局部变量的副本中的值。 然后再清除对应的key为null的value。
3、remove方法:先将Entry中对key的弱引用断开,设置为null,然后再清除对应的key为null的value。
其中get方法和set方法在此文章有详细说明
https://www.cnblogs.com/webor2006/p/13168512.html
5、ThreadLocal内存泄露问题本质分析
就拿例子中thread1来讲,虚拟机栈中会存在thread1和threadLocal的引用分别指向对应的Thread1对象和ThreadLocal对象,而Entry是一个弱引用标识的
1、假设Entry是用强引用标识会有什么问题
image.png在某种场景下,持有ThreadLocal引用的对象不需要ThreadLocal了,肯定栈中的引用需要断掉
image.png
那照理堆中的它得被垃圾收集器给回收:
image.png
但是!!!此时从图中可以看到它有被其它对象强引用的,也就是ThreadLocalMap当中的Entry数组中的key所强引用着的,那造成的影响就是这个ThreadLocal对象永远也得不到回收,它得不到回收,那么这个Entry数组的元素永远会持续的只增不减,而糟糕的是此时由于栈中已经没有引用指向该ThreadLocal对象了,那么该ThreadLocal对象以及指向它的Entry数组中的key和value会永远在堆中占据着空间得不到释放,因此就导致内存泄漏发生了
只增不减的原因:例如只是用户ID这个ThreadLocal不再使用了,而其他的ThreadLoca(用户名字的ThreadLocal)却在该线程的ThreadLocalMap中继续添加,而用户ID的ThreadLocal却不使用,因此是只增不减
同样,若Entry是用软引用标识也是如此,在内存不爆的情况下和强引用没什么区别,内存爆了才会回收,会增加GC的次数
2、Entry是用弱引用标识的具体情况和注意事项
Entry使用弱引用标识还是会出现内存泄露问题,为什么?理由如下
一开始
image.png
在某种场景下,持有ThreadLocal引用的对象不需要ThreadLocal了,肯定栈中的引用需要断掉
image.png
而此时堆中的ThreadLocal对象只被一个Entry为key的弱引用所指向了,而根据弱引用回收的特点:下一次垃圾回收时一定会被回收掉
就变成了
image.png
新的问题又出现了!!!既然key为null了,那。。
image.png
问题:为什么Value不定义成弱引用?如果value是弱引用,key弱引用指向的对象回收的时候,value弱引用指向的对象跟着回收掉不就好了??
答案是:Value不能定义成弱引用,如果value定义为弱引用,当GC的时候就会直接将这个Value干掉了,而此时我们的ThreadLocal还处于使用期间(ThreadLocal被强引用指向,被弱引用指向,因此清除不掉),就会造成Value为null的错误,所以将其设置为强引用
解决方案:
即使Entry是用弱引用标识,还是会出现内存泄露的问题,不过这个Value的内存泄漏Java的设计者已经帮我们考虑到了,在ThreadLocal类中存3个常用的方法,调用的时候会自动检查清除key为null的value
1、set方法:将当前线程的此线程局部变量的副本设置为指定的值。然后再清除对应的key为null的value。
2、get方法:返回当前线程的此线程局部变量的副本中的值。 然后再清除对应的key为null的value。
3、remove方法:先将Entry中对key的弱引用断开,设置为null,然后再清除对应的key为null的value。
还是会存着一种情况,我们之前有使用过ThreadLocal对象的set和get方法,因此使用过的ThreadLocal对象是在堆中存在着的,后面我们已经不再使用这个ThreadLocal对象了,换言之,我们不会再去调set和get方法,因此就不会去进行检查哪些key == null的对象加以回收,那这个ThreadLocalMap中的key和value还是会存在堆中直到线程结束(该ThreadLocal对象可能被其他线程用着),还是会造成内存泄露的问题
当我们使用完ThreadLocal的时候,最好调用一下remove方法,remove()方法可以发挥很好的作用,习惯性地调用remove()方法将Entry中对key的弱引用断开,设置为null,然后再清除对应的key为null的value。
image.png