一、使用过ThreadLocal吗?说说ThreadLocal 的作用
ThreadLocal是解决线程安全问题一个很好的思路,使用ThreadLocal操作变量,对于线程都是私有的,保证了线程安全问题。
二、说说ThreadLocal 实现原理
日常使用ThreadLocal一般如下:
2.1 看看ThreadLocal set()和get()方法都干了啥:
/**
*ThreadLocal get() 方法
*/
public T get() {
//获得当前线程
Thread t = Thread.currentThread();
//获取线程中的ThreadLocalMap对象(一个Map)
ThreadLocalMap map = getMap(t);
if (map != null) {
//key为 ThreadLocal对象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value; //如何不为空,获取value值
return result;
}
}
return setInitialValue();//未初始化,完成初始化,并返回
}
/**
* ThreadLocal getMap() 方法
*/
ThreadLocalMap getMap(Thread t) {
//获取Thread(线程)中的threadLocals(一个Map对象)
return t.threadLocals;
}
/**
*ThreadLocal set() 方法
*/
public void set(T value) {
Thread t = Thread.currentThread();
//获得当前线程
ThreadLocalMap map = getMap(t);
//获取线程中的ThreadLocalMap对象(一个Map)
if (map != null)
// ThreadLocalMap不为空,将value设置到Map中,key为ThreadLocal map.set(this, value);
else
//如果为空,先初始化ThreadLocalMap ,然后再设置值
createMap(t, value);
}
2.2 Thread源码:
public class Thread implements Runnable {
.......
/**
* Thread 当中有一个成员变量,threadLocals(是一个ThreadLocalMap对象 )
* ThreadLocalMap其实就是一个Map对象
* 每一个线程都有一个Map来存数据,所以当然线程安全了。
* ThreadLocal 每次调用get或者set方法,最后都是为了获取 Thread中的
* threadLocals(Map),然后存或者取数据
*/
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
2.3 基本流程如下:
总结:
ThreadLocal 是一个入口对象,每次操作ThreadLocal 进行get() 和set() 操作,底层其实是获取 Thread对象中的ThreadLocalMap(一个Map)对象进行操作(Map都用过,不用说了)
三、ThreadLocal是如何实现线程安全的?
首先,要明白,实现线程安全其实底层并不是ThreadLocal的能力,而是Thread对象的能力。
ThreadLocal不过是一个入口对象,操作ThreadLocal其实是为了获取Thread对象。
3.1 总结:
1.线程安全是Thread底层的能力,可以实现线程安全,其实是因为每一个线程中都有一个ThreadLocalMap(Map)对象,每个线程有自己的Map对象存数据和取数据,所以线程安全。
2. ThreadLocal是一个入口对象,最终目的是先获取Thread对象,然后在获取Thread中的ThreadLocalMap对象。
四、ThreadLocalMap的底层结构?
数据结构如下:
ThreadLocalMap的数据结构其实是很像HashMap,但是也有一些差异:
- 1. 它并未实现Map接口,而且他的Entry是继承WeakReference(弱引用)的。
- 2. 没有像HashMap有next指针,所以ThreadLocalMap不存在链表。
五、ThreadLocalMap底层为什么需要数组?没有了链表它是如何解决Hash冲突的?
为什么需要数组?看看这个代码:
最终Thread中的threadLocals对象为:
一个线程可以有多个TreadLocal来存放不同类型的对象,所以需要数组来存。
5.1 没有了链表怎么解决Hash冲突的?
5.2 新增流程图:
5.3 总结:
先根据key(ThreadLocal对象)计算出在数组中的index位置:
- 如果index为空,直接新建一个Entry对象放index位置上
- 如果ind ex不为空,key(ThreadLocal)对象相等,直接更新 Entry中value值
- 如果index不为空,且key(ThreadLocal)对象不相等(出现hash冲突了),那就找下一个空位置,直到为空为止。
六、如何共享线程的ThreadLocal数据?
线程在初始化的时候,会将父线程的InheritableThreadLocal值复制给子线程,实现了父子线程共享ThreadLocal中的数据。所以我们可以在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。
七、说说什么是强引用,什么是弱引用?
ThreadLocalMap的key 采用了弱引用,首先我们需要先理解什么是强引用弱引用。最后才能明白ThreadLocal 内存泄露原理
7.1 JVM判断对象是否存活一般有两种方式:
7.1.1 引用计数算法
每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
7.1.2 可达性分析(Reachability Analysis):
从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。GC Roots包括
- 虚拟机栈中引用的对象。
- 方法区中类静态属性实体引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI引用的对象。
如果判断对象非存活状态就会被垃圾回收器回收掉(这里先简单介绍,不对JVM和垃圾回收做过多介绍,关注我,后面我会专门写文章详细介绍)
7.2 Java对象引用的4种级别
Java对象的引用被划分为4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用
强引用: 默认是强引用,也就是我们经常干的事情。疯狂new,new,new。这个时候创建的对象都是强引用 。 这是Java中最常见的引用,在没有使用特殊引用的情况下,都是强引用,比如Object o = new Object()就是典型的强引用。能让程序员通过强引用访问到的对象,不会被JVM垃圾回收,即使内存空间不够,JVM也不会回收这些对象,而是抛出内存溢出异常;
软引用: 软引用描述的是一些还有用,但不是必须的对象。被软引用所引用的对象,也不会被垃圾回收,直到JVM将要发生内存溢出异常时,才会将这些对象列为回收对象,进行回收。在JDK1.2之后,提供了SoftReference类实现软引用;
弱引用:弱引用描述的是非必须的对象,被弱引用所引用的对象,只能生存到下一次垃圾回收前,下一次垃圾回收来临,此对象就会被回收。在JDK1.2之后,提供了WeakReference类实现弱引用(也就是上面Entry继承的类);
虚引用: 这是最弱的一种引用关系,一个对象是否有虚引用,完全不会对其生存时间产生影响,我们也不能通过一个虚引用访问对象,使用虚引用的唯一目的就是,能在这个对象被回收时,受到一个系统的通知。JDK1.2之后,提供了PhantomReference实现虚引用;
我们平时常用的对象都是强引用对象,只要对象还在被其他对象引用,就不会被垃圾回收器回收。
八、在使用ThreadLocal中有出现内存泄露吗?如何处理?
同问:有的面试官会问在使用ThreadLocal中你有发现ThreadLocal的什么问题吗?大多数其实就是想问你内存泄露问题。
static class ThreadLocalMap {
/**
*
* ThreadLocalMap 是Thread中的一个内部类,ThreadLocalMap 和普通Map对象相差不大 (差异:key是弱引用)
* key继承了WeakReference,所以key是弱引用,而value是普通的强引用
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**下面这些参数和普通Map对象差异不大,*/
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0;
private int threshold;
首先我们看看ThreadLocalMap的源码,ThreadLocalMap中的key继承了WeakReference,所以key是弱引用,而value是普通的强引用。
分析一下以下代码的引用关系:
8.1 引用关系如图:
以上代码存在这几条引用链:
- ThreadLocal对象被Test对象引用(强引用)和 ThreadLocalMap对象引用(弱引用)。
Test对象代码运行完毕,Test对象被垃圾回收,失去对ThreadLocal的引用。而ThreadLocalMap对ThreadLocal对象是弱引用(垃圾回收的时候直接回收掉) 。
- value对象被ThreadLocalMap引用,ThreadLocalMap又被Thread引用。所以只要Thread(线程对象)不被销毁,value对象就不能被垃圾回收器回收。
线程池里面的线程都是复用的,操作系统新建的线程也会进行复用!所以Thread(线程对象)有可能一直循环使用,不会销毁。Thread(线程对象)不被销毁,value对象一直无法被垃圾回收器回收,从而导致了内存泄露。
8.2 ThreadLocal 如何解决内存泄露问题?
Thread对象被线程池控制,我们很难干预。所以我们关键解决思路是去掉ThreadLocalMap对value对象的引用。
当ThreadLocal(key)被垃圾回收后,ThreadLocalMap的key变为了null, 但value值依然存在:
8.2.1 方法一:ThreadLocal针对这个问题底层本身有做处理:
在ThreadLocal底层实现中,set/getEntry方法中,线程若检测到key为null的元素,会将此元素的value置为null,然后将这个元素从ThreadLocalMap中删除(具体源码这里暂不分析,有兴趣可以去看看源码)。
但是需要依赖调用ThreadLocal的set/getEntry等方法。假如ThreadLocal设置了很大的value对象后,一直未调用set/getEntry等方法,key被垃圾回收后,value却一直得不到回收。依然会出现内存泄露,所以也不能完全依赖ThreadLocal的底层实现,需要靠我们手动remove。
8.2.2 方法二:使用完ThreadLocal对象后手动remove
8.3 为什么需要remove?除了解决内存泄露还有什么作用?
先看看下面代码:思考一下会不会出现什么问题?
结果:理论上每请求一次 /test接口就会打印 11111。但实际上可能不会。
有人会想,ThreadLocal不是线程安全的吗?怎么有时候会产生非线程安全的错觉?其实不是线程安全的问题。操作系统的线程其实是会复用的。
如:
第一个请求过来分配线程1处理。 第二个请求过来,可能线程1刚好处理完了请求1。线程1被复用,继续处理请求2,ThreadLocal使用完并没有remove掉数据,所以false任然存在
线程1的ThreadLocalMap中。 请求2调用threadLocal2.get(),会获得false(请求1放进去的),所以后面的请求可能会继续打印 11111,也可能不会打印。关键在于操作系统分配处理请求的线程,是不是前面请求的,线程中是否已经存在值。
8.4 总结:
所以,我们在使用ThreadLocal时,应注意:
- 每次调用开始前,调用set()方法,更新ThreadLocalMap中的数据
- 推荐: 每次使用后调用remove()方法清除数据,既可以避免内存内存泄露问题,也可以避免上诉问题。
九、ThreadLocalMap设计的时候为什么不把value也设计成弱引用?
首先,有人可能会有这样的疑问,ThreadLocalMap 的key设计成了弱引用,而弱引用在垃圾回收的时候就会被回收掉,如果我写的程序运行时间较长,岂不是很危险。程序运行到一半,ThreadLocal对象就被回收了?
看看上面我分析过的代码:
引用关系:
为什么不会有问题?
虽然ThreadLocalMap 对key(ThreadLocal)是弱引用,但是业务代码Test对象对key(ThreadLocal)对象是强引用。所以ThreadLocal不会被垃圾回收,直到业务代码Test对象运行完毕被垃圾回收,然后才会回收ThreadLocal对象。这也是ThreadLocal设计的精妙的地方。ThreadLocal对象随业务代码回收而回收。
那为何Value不设置成弱引用?
因为不清楚这个Value
除了ThreadLocalMap
的引用还是否还存在其他引用,如果不存在其他引用,当GC
的时候就会直接将这个Value干掉了,而此时我们的ThreadLocal
还处于使用期间,就会造成Value为null的错误,所以将其设置为强引用。
如下:
Test对象将 hashMap传给了 Test2对象,Test对象去掉了对HashMap的引用。因此最终只剩下 Test2对象中的 threadLocal1.ThreadLocalMap对hashMap有引用。
假如将ThreadLocalMap的value引用改成弱引用,hashMap将随时被垃圾回收器回收掉。
但是Test2对象的程序还在运行,可能运行到中途value就被回收掉了,导致Test2中取不到value值了。所以不能将value对象的引用设计成弱引用!