最近在面试一家还算二三线的略知名厂时,被问到了ThreadLocal,虽然大致的使用方式是回答出来了,但面试官问到使用ThreadLocal需要注意什么,以及它为什么会导致内存泄漏的问题时,就答不好了。
所以写篇文章记录一下。
ThreadLocal本身并不复杂,但面试一旦被问到,拖个5-10分钟还是没问题的,感觉也算是一个有“性价比”的知识点。
首先需要一点前置知识,Java中实际上有4种引用。
public class MyObject {
@Override
protected void finalize() throws Throwable {
System.out.println("finalize");
super.finalize();
}
}
没有强引用指向的对象会被gc回收掉,反过来说就是有强引用指向的对象一定不会被回收,这就是”强“的意思。
public class NormalRef {
public static void main(String[] args) {
MyObject myObject = new MyObject();
myObject = null;
System.gc();
}
}
运行这段代码,可以看到,将强引用赋null后,myObject没有强引用指向,因此在调用gc后被回收,打印”finalize“。
2.软引用
看代码
public class SoftRef {
public static void main(String[] args) throws InterruptedException {
// 一个软引用指向一个10M的数组
SoftReference<byte[]> softReference = new SoftReference<>(new byte[1024 * 1024 * 10]);
System.gc();
System.out.println(softReference.get()); // 可以看到 软引用没有被回收
Thread.sleep(2000);
// new 一个12m的对象,此时内存不够,发生gc
byte[] bytes = new byte[1024 * 1024 * 12];
System.out.println(softReference.get());// 内存不够用时 软引用被回收
}
}
执行这段程序时,需要设置jvm的一些参数:最大堆20m,打印gc信息
可以看到,第一次gc时,有软引用指向数组,不会被回收;第二次内存不够时,软引用被回收。
软引用常被使用于做缓存,比如读取一个大图片100m,当我内存一直够用时,那就一直放在内存中,当内存不够时,就把这块内存回收。
public class WeakRef {
public static void main(String[] args) {
WeakReference<MyObject> weakReference = new WeakReference<>(new MyObject());
System.gc();
System.out.println(weakReference.get());
}
}
弱引用很简单,只要遭遇gc,就会被回收。
那弱引用一gc就被回收,还有什么作用呢?实际上常用于容器中(先记住结论)
引申:WeakHashMap
public class PhantomRef {
// 定义一个list,等下不断的往list中扔值触发gc
static List<Object> list = new LinkedList<>();
// 定义一个引用队列(阻塞式的)
static ReferenceQueue<MyObject> queue = new ReferenceQueue<>();
public static void main(String[] args) {
// 虚引用指向MyObject,同时传入一个引用队列
PhantomReference<MyObject> myObjectPhantomReference = new PhantomReference<MyObject>(new MyObject(),queue);
new Thread(()->{
while(true){
// 不断往list里家数据,模拟内存满了gc
list.add(new byte[1024*1024]);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 虚引用永远获取不到值
System.out.println(myObjectPhantomReference.get());
}
}).start();
// 另一个线程不断的从队列里拿数据
// 虚引用指向的对象被回收后,会自动把这个引用加入队列
new Thread(()->{
while (true){
Reference<? extends MyObject> ref = queue.poll();
if(ref != null){
System.out.println(ref);
System.out.println("对象被回收了");
}
}
}).start();
}
}
ThreadLocal,即线程本地变量。是一个以ThreadLocal变量为键,任意对象为值的数据结构。有set、get方法。可以理解为线程独有的一块变量。
应用场景:方法计时,代码如下
public class Profiler {
// 线程本地变量,保存的是时间的值
public static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<>();
public static final void begin(){
TIME_THREADLOCAL.set(System.currentTimeMillis());
}
public static final long end(){
return System.currentTimeMillis() - TIME_THREADLOCAL.get();
}
public static void main(String[] args) throws InterruptedException {
Profiler.begin();
TimeUnit.SECONDS.sleep(1);
System.out.println(end());
}
}
这是一个计时器的工具类。假设不用ThreadLocal会怎样?在并发场景下Profiler的静态时间变量可能会被2个线程同时调用begin()而导致其中一个线程的数据被覆盖。所以用了ThreadLocal对时间变量做包装,这样每一个线程的数据都是线程私有的,不会产生覆盖写这种并发问题
可以定义一个切面,对所有Controller层的方法进行拦截,joinPoint(AOP中的概念)获取方法名+在方法调用的前后调用上面工具类的Start和end,打印方法执行时间
set源码 先看set方法
点进getMap,发现实际获取的是Thread下的一个变量map,这也能解释为什么是线程私有的了。
总结一下set就是:
先获取当前线程
再通过线程获取线程私有的ThreadLocalMap,这个ThreadLocalMap是ThreadLocal的一个内部类
再往ThreadLocalMap里塞值,其中key又是当前ThreadLocal的引用,V是具体的值
相信这个会让人觉得绕来绕去的,特别是往Map里塞值为什么又要是ThreadLocal自己的引用会让人觉得懵逼,实际仔细捋捋就知道了:
ThreadLocalMap是Thread的一个字段,说明这个map是线程私有的,那也代表着一个线程只有一个map。此时如果有多个ThreadLocal变量,那在同一个线程中获取的就是同一个map,那要怎么获取不同ThreadLocal对应的值呢?就是把key设置成threadLocal!
依然是获取当前线程的map
通过this的key获取对应Entry
再通过Entry获取Value
如果map没有设置值,则设置默认值并返回
看完set和get,好像很简单的亚子。ThreadLocal和4种引用又是什么关系?
别急,下面才是重头!
上文只讲了有一个线程私有的map,map里的key是threadLocal的引用,但map的结构是什么样子的?
ThreadLocalMap源码
从上面至少可以看出
ThreadLocalMap是ThreadLocal的一个内部类
ThreadLocalMap有一个Entry的内部类(Entry就是Map中的一个k、v键值对)
这个Entry很重要,可能看它的源码又会懵逼:Entry怎么继承了一个弱引用
解释:
假设有一个ThreadLocal类型的变量tl,进行set操作
首先,是往当前线程的map里放值
这个Map的每一个键值对都是一个Entry,这个Entry的弱引用指向了一个key,Entry中有一个Value
那为什么Entry要用一个弱引用指向ThreadLocal?
原因:
现在假设Entry同普通Map一样,有一个Key字段。
假设有一个ThreadLocal局部变量tl,那这个tl应该在方法执行完毕后变为垃圾对象,局部变量tl指向ThreadLocal实例的引用消失
但由于ThreadLocalMap是线程私有的一个字段,而线程有时是不会回收的(比如线程池的核心线程),或是线程存活时间很长,这个map将一直存在
而map里的key指向了这个ThreadLocal的实例,因此ThreadLocal实例依然有强引用存在,无法被垃圾回收!
但实际上这个threadLocal实例在方法结束后就不会再用到了!应该被回收!这就是内存泄漏
所以用一个弱引用指向ThreadLocal实例,在强引用还存在时,不会被回收,强引用一旦消失,这个实例对象就会被回收。这样好像就完美了。
但,依然没有这么简单。假设现在threadLocal实例被回收了,此时Map中的Entry还是存在的,只是Entry中的key变为了null,但value还是一直存在!
若创建了很多个ThreadLocal实例,实际上就是创建了很多的Entry在map中,就算threadLocal被回收,但map中的entry和entry中的value不会被回收!依然会可能出现内存泄漏。
因此,使用ThreadLocal注意的情况为:
在每一次使用完ThreadLocal后,都要手动调用一下ThreadLocal.remove()方法,将当前Entry 删除,防止内存泄漏