目录
ThreadLocalMap中的key为什么要设置为弱引用?
ThreadLocal是什么?
意如其名:本地线程变量
ThreadLocal变量是当前线程的变量,该变量对其他线程都是隔离的,ThreadLocal变量为每个线程都创建了一个内部变量。每个线程都可以访问自己的内部变量,其他线程不可访问。
ThreadLocal的作用?
- ThreadLocal可以实现线程间的隔离,实现线程安全
- ThreadLocal中每个线程可以实现资源共享(线程内)
ThreadLocal实现原理
ThreadLocal每个线程内都有一个内部的成员变量 ThreadLocalMap
起到线程隔离的是每个线程内的map集合,threadLocal起到的作用的关联资源对象
ThreadLocalMap这个变量用于存储资源对象
- 调用set方法,就是以当前ThreadLocal作为Key,资源对象为Value,存入ThreadLocalMap中
- 调用get方法,就是以当前ThreadLocal作为Key,从资源对象中获取相对应的资源
- 调用remove方法,就是以当前ThreadLocal作为Key,从资源对象中移除相对应的资源
ThreadLocalMap
由图可以看出 每个线程都有一个内部的map,并且都有独立的标识(1,2,3)
ThreadLocal就是通过这些对线程进行隔离的
ThreadlocalMap的初始容量capcity = 16,(负载因子)factor = 2/3
当元素超过 16 * 2/3 = 10.66666 = 10(向下取整)时扩容
ThreadLocalMap是如何处理hash冲突的?
在别的map中(hashmap,hashtable)等等中都是采用拉链法来处理hash冲突的
但是在ThreadLocalMap是采用开放寻址法解决hash冲突的
什么是开放寻址法?
比如在上图中thread-1中加入元素,当与key为a索引为0的元素进行了hash冲突,将会把元素放在下一个空闲的位置
如图可见,与索引为0的元素出现hash冲突就将元素放在了下一个空闲的位置
ThreadLocalMap中的key为什么要设置为弱引用?
先查看源码(JDK11)
static class ThreadLocalMap {
private static final int INITIAL_CAPACITY = 16;
private ThreadLocal.ThreadLocalMap.Entry[] table;
private int size = 0;
private int threshold;
private void setThreshold(int len) {
this.threshold = len * 2 / 3;
}
private static int nextIndex(int i, int len) {
return i + 1 < len ? i + 1 : 0;
}
private static int prevIndex(int i, int len) {
return i - 1 >= 0 ? i - 1 : len - 1;
}
进入Entry数组中查看
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
this.value = v;
}
}
发现只有键super(k)调用了父类的构造(WeakRenference)把他变成了一个弱引用
为什么要这样设计呢?
仔细看下ThreadLocal内存结构就会发现,Entry数组对象通过ThreadLocalMap最终被Thread持有,并且是强引用。也就是说Entry数组对象的生命周期和当前线程一样。即使ThreadLocal对象被回收了,Entry数组对象也不一定被回收,这样就有可能发生内存泄漏。ThreadLocal在设计的时候就提供了一些补救措施:
- Entry的key是弱引用的ThreadLocal对象,很容易被回收,导致key为null(但是value不为null)。所以在调用get()、set(T)、remove()等方法的时候,会自动清理key为null的Entity。
- remove()方法就是用来清理无用对象,防止内存泄漏的。所以每次用完ThreadLocal后需要手动remove()。
- 有些文章认为是弱引用导致了内存泄漏,其实是不对的。假设把弱引用变成强引用,这样无用的对象key和value都不为null,反而不利于GC,只能通过remove()方法手动清理,或者等待线程结束生命周期。也就是说ThreadLocalMap的生命周期由持有它的线程来决定,线程如果不进入terminated状态,ThreadLocalMap就不会被GC回收,这才是ThreadLocal内存泄露的原因。
- Thread可能要进行长时间的运行(如线程池中的线程),如果key不再使用,需要在内存不足GC时回收,释放其占用的内存,如果是强引用不再使用的key也回收不了
- GC仅仅是让key的内存释放,值内存的释放还需要酌情处理,释放时机:
- 获取(get) key发现null key(不推荐,容易造成内存泄漏)
- 设置(set) key时会使用启发式扫描,清除临近的null key,启发次数与元素个数,是否发现null key 有关(不推荐,容易造成内存泄漏)
- remove时(推荐),因为一般使用ThreadLocal时都把它作为静态变量,因此GC无法回收
参考文献:Java面试题视频教程,大厂Java面试突击技巧,工作几年和应届生必看的黑马程序员Java面试必考真题_哔哩哔哩_bilibili
ThreadLocal是什么?怎么用?为什么用它?有什么缺点? - 知乎 (zhihu.com)