ThreadLocal详解(附面试题)

4.2 get方法

对应源码:

/**

  • 返回当前线程中保存ThreadLocal的值

*/

public T get() {

// 获取当前线程对象

Thread t = Thread.currentThread();

// 获取此线程对象中维护的ThreadLocalMap对象

ThreadLocalMap map = getMap(t);

// 如果此map存在

if (map != null) {

// 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e

ThreadLocalMap.Entry e = map.getEntry(this);

// 对e进行判空

if (e != null) {

@SuppressWarnings(“unchecked”)

// 获取存储实体 e 对应的 value值

// 即为我们想要的当前线程对应此ThreadLocal的值

T result = (T)e.value;

return result;

}

}

/*

初始化 : 有两种情况有执行当前代码

第一种情况: map不存在,表示此线程没有维护的ThreadLocalMap对象

第二种情况: map存在, 但是没有与当前ThreadLocal关联的entry

*/

return setInitialValue();

}

/**

  • 初始化

  • @return the initial value 初始化后的值

*/

private T setInitialValue() {

// 调用initialValue获取初始化的值

// 此方法可以被子类重写, 如果不重写默认返回null

T value = initialValue();

// 获取当前线程对象

Thread t = Thread.currentThread();

// 获取此线程对象中维护的ThreadLocalMap对象

ThreadLocalMap map = getMap(t);

// 判断map是否存在

if (map != null)

// 存在则调用map.set设置此实体entry

map.set(this, value);

else

// 1)当前线程Thread 不存在ThreadLocalMap对象

// 2)则调用createMap进行ThreadLocalMap对象的初始化

// 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中

createMap(t, value);

// 返回设置的值value

return value;

}

整体可以分为几步:

  1. 首先获取当前线程, 根据当前线程获取一个Map

  2. 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entry e,否则转到4

  3. 如果e不为null,则返回e.value,否则转到4

  4. Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map

总结成一句话就是:先获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始值。

4.3 remove方法

对应源码:

/**

  • 删除当前线程中保存的ThreadLocal对应的实体entry

*/

public void remove() {

// 获取当前线程对象中维护的ThreadLocalMap对象

ThreadLocalMap m = getMap(Thread.currentThread());

// 如果此map存在

if (m != null)

// 存在则调用map.remove

// 以当前ThreadLocal为key删除对应的实体entry

m.remove(this);

}

整体可以分为几步:

  1. 首先获取当前线程,并根据当前线程获取一个Map

  2. 如果获取的Map不为空,则移除当前ThreadLocal对象对应的entry

4.4 initialValue方法

对应源码:

/**

  • 返回当前线程对应的ThreadLocal的初始值

  • 此方法的第一次调用发生在,当线程通过get方法访问此线程的ThreadLocal值时

  • 除非线程先调用了set方法,在这种情况下,initialValue 才不会被这个线程调用。

  • 通常情况下,每个线程最多调用一次这个方法。

  • 这个方法仅仅简单的返回null {@code null};

  • 如果程序员想ThreadLocal线程局部变量有一个除null以外的初始值,

  • 必须通过子类继承{@code ThreadLocal} 的方式去重写此方法

  • 通常, 可以通过匿名内部类的方式实现

  • @return 当前ThreadLocal的初始值

*/

protected T initialValue() {

return null;

}

此方法的作用:返回该线程局部变量的初始值。

(1) 这个方法是一个延迟调用方法,从上面的代码我们得知,在set方法还未调用而先调用了get方法时才执行,并且仅执行1次

(2)这个方法缺省实现直接返回一个null

(3)如果想要一个除null之外的初始值,可以重写此方法。(备注: 该方法是一个protected的方法,显然是为了让子类覆盖而设计的)

5.ThreadLocalMap 源码分析


5.1 基本结构

ThreadLocalMapThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现。如下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z5s2krCM-1641381418228)(ThreadLocal详解.assets/20210124143602480.png)]

成员变量

/**

  • 初始容量 —— 必须是2的整次幂

*/

private static final int INITIAL_CAPACITY = 16;

/**

  • 存放数据的table,Entry类的定义在下面分析

  • 同样,数组长度必须是2的整次幂。

*/

private Entry[] table;

/**

  • 数组里面entrys的个数,可以用于判断table当前使用量是否超过阈值。

*/

private int size = 0;

/**

  • 进行扩容的阈值,表使用量大于它的时候进行扩容。

*/

private int threshold; // Default to 0

跟HashMap类似:

  • INITIAL_CAPACITY代表这个Map的初始容量

  • table 是一个Entry 类型的数组,用于存储数据

  • size 代表表中的存储数目

  • threshold 代表需要扩容时对应 size 的阈值

存储结构 - Entry

/*

  • Entry继承WeakReference,并且用ThreadLocal作为key

  • 如果key为null(entry.get() == null),意味着key不再被引用

  • 因此这时候entry也可以从table中清除

*/

static class Entry extends WeakReference<ThreadLocal<?>> {

/** The value associated with this ThreadLocal. */

Object value;

Entry(ThreadLocal<?> k, Object v) {

super(k);

value = v;

}

}

从源码就可以看出:

  • ThreadLocalMap中,也是用Entry来保存K-V结构数据的。不过Entry中的key只能是ThreadLocal对象,这点在构造方法中已经限定死了

  • 另外,Entry继承WeakReference,也就是key(ThreadLocal)是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑

5.2 弱引用和内存泄漏

先来了解一些概念:

内存相关概念

  • Memory overflow:内存溢出,没有足够的内存提供申请者使用。

  • Memory leak:内存泄漏,是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出。

弱引用相关概念

关于Java中的引用可以参考我的这篇文章:【JVM】垃圾回收1-3节

我们接下来看几个问题,具体了解弱引用和内存泄漏:

① 我们先来看看如果key使用强引用会出现什么问题?

假设ThreadLocalMap中的key使用了强引用,此时ThreadLocal的内存图(实线表示强引用)如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wdbcUx4D-1641381418230)(ThreadLocal详解.assets/20210124144330949.png)]

  1. 假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。

  2. 但是因为threadLocalMap的Entry强引用了threadLocal,造成threadLocal无法被回收。

  3. 在没有手动删除这个Entry以及CurrentThread依然运行的前提下,始终有强引用链 threadRef->currentThread->threadLocalMap->entry,Entry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。

  4. 也就是说,ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的

② 我们在看看如果key使用弱引用呢?

ThreadLocalMap中的key使用了弱引用,此时ThreadLocal的内存图(实线表示强引用,虚线表示弱引用)如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fgYTy0fl-1641381418236)(ThreadLocal详解.assets/20210124144945153.png)]

  • 同样假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。

  • 由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收,此时Entry中的key=null

  • 但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry -> value ,value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏。

  • 也就是说,ThreadLocalMap中的key使用了弱引用, 也有可能内存泄漏。

③ 内存泄漏的真实原因是什么呢?

比较以上两种情况,我们就会发现,内存泄漏的发生跟ThreadLocalMap中的key是否使用弱引用是没有关系的。那么内存泄漏的的真正原因是什么呢?

如果想避免内存泄漏,都有两个前提:

  1. 只要在使用完ThreadLocal,调用其remove方法删除对应的Entry,就能避免内存泄漏。

  2. 由于ThreadLocalMap是Thread的一个属性,被当前线程所引用,所以它的生命周期跟Thread一样长。那么在使用完ThreadLocal之后,如果当前Thread也随之执行结束,ThreadLocalMap自然也会被gc回收,从根源上避免了内存泄漏。

综上,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏。

④ 为什么使用弱引用?

根据刚才的分析,我们知道了:无论ThreadLocalMap中的key使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。

要避免内存泄漏有两种方式:

  1. 使用完ThreadLocal,调用其remove方法删除对应的Entry

  2. 使用完ThreadLocal,当前Thread也随之运行结束

也就是说,只要记得在使用完ThreadLocal及时的调用remove,无论key是强引用还是弱引用都不会有问题。那么为什么key要用弱引用呢?

  1. 事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。

  2. 这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:使用完ThreadLocal,弱引用的ThreadLocal会被回收,key就会变成null,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。

5.3 hash冲突的解决

ThreadLocal的set()

public void set(T value) {

Thread t = Thread.currentThread();

ThreadLocal.ThreadLocalMap map = getMap(t);

if (map != null)

//调用了ThreadLocalMap的set方法

map.set(this, value);

else

createMap(t, value);

}

ThreadLocal.ThreadLocalMap getMap(Thread t) {

return t.threadLocals;

}

void createMap(Thread t, T firstValue) {

//调用了ThreadLocalMap的构造方法

t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);

}

这个方法刚才分析过, 其作用是设置当前线程绑定的局部变量:

  1. 首先获取当前线程,并根据当前线程获取一个Map

  2. 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)(这里调用了ThreadLocalMap的set方法)

  3. 如果Map为空,则给该线程创建 Map,并设置初始值 (这里调用了ThreadLocalMap的构造方法)

这段代码有两个地方分别涉及到ThreadLocalMap的两个方法, 我们接着分析这两个方法。

构造方法 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)

/*

  • firstKey : ThreadLocal实例(this)

  • firstValue : 要保存的线程本地变量

*/

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {

//初始化table

table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];

//计算索引(重点代码)

int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

//设置值

table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);

size = 1;

//设置阈值

setThreshold(INITIAL_CAPACITY);

}

整个初始化过程就是:

  1. 首先创建一个长度为16的Entry数组

  2. 然后计算出firstKey对应的索引

  3. 然后存储到table中

  4. 最后设置size和threshold

我们这里重点分析int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

① 首先关于firstKey.threadLocalHashCode

private final int threadLocalHashCode = nextHashCode();

private static int nextHashCode() {

return nextHashCode.getAndAdd(HASH_INCREMENT);

}

//AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减,适合高并发情况下的使用

private static AtomicInteger nextHashCode = new AtomicInteger();

//特殊的hash值

private static final int HASH_INCREMENT = 0x61c88647;

这里定义了一个AtomicInteger类型,每次获取当前值并加上HASH_INCREMENTHASH_INCREMENT = 0x61c88647,这个值跟斐波那契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里,也就是Entry[] table中,这样做可以尽量避免hash冲突。

② 然后关于& (INITIAL_CAPACITY - 1)

计算hash的时候里面采用了hashCode & (size - 1)的算法,这相当于取模运算hashCode % size的一个更高效的实现。正是因为这种算法,我们要求size必须是2的整次幂,这也能保证在索引不越界的前提下,使得hash发生冲突的次数减小。

ThreadLocalMap中的set方法

private void set(ThreadLocal<?> key, Object value) {

ThreadLocal.ThreadLocalMap.Entry[] tab = table;

int len = tab.length;

//计算索引(重点代码,刚才分析过了)

int i = key.threadLocalHashCode & (len-1);

/**

  • 使用线性探测法查找元素(重点代码)

*/

for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];

e != null;

e = tab[i = nextIndex(i, len)]) {

ThreadLocal<?> k = e.get();

//ThreadLocal 对应的 key 存在,直接覆盖之前的值

if (k == key) {

e.value = value;

return;

}

// key为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了,

// 当前数组中的 Entry 是一个陈旧(stale)的元素

if (k == null) {

//用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏

replaceStaleEntry(key, value, i);

return;

}

}

//ThreadLocal对应的key不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的Entry。

tab[i] = new Entry(key, value);

int sz = ++size;

/**

  • cleanSomeSlots用于清除那些e.get()==null的元素,

  • 这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置null。

  • 如果没有清除任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行rehash(执行一次全表的扫描清理工作)

*/

if (!cleanSomeSlots(i, sz) && sz >= threshold)

rehash();

}

/**

  • 获取环形数组的下一个索引

*/

private static int nextIndex(int i, int len) {

return ((i + 1 < len) ? i + 1 : 0);

}

整个执行流程:

  1. 首先还是根据key计算出索引 i,然后查找i位置上的Entry

  2. 若是Entry已经存在并且key等于传入的key,那么这时候直接给这个Entry赋新的value值

  3. 若是Entry存在,但是key为null,则调用replaceStaleEntry来更换这个key为空的Entry

  4. 不断循环检测,直到遇到为null的地方,这时候要是还没在循环过程中return,那么就在这个null的位置新建一个Entry,并且插入,同时size增加1

  5. 最后调用cleanSomeSlots,清理key为null的Entry,最后返回是否清理了Entry,接下来再判断sz >= thresgold,如果没有清除任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行rehash执行一次全表的扫描清理

ThreadLocalMap这里使用线性探测法来解决哈希冲突的:

该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。

举个例子,假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。按照上面的描述,可以把Entry[] table看成一个环形数组。

6.常见面试题总结


6.1 什么是ThreadLocal?有哪些应用场景?

ThreadLocal 是JDK java.lang 包下的一个类,ThreadLocal 为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量,并且不会和其他线程的局部变量冲突,实现了线程间的数据隔离。

ThreadLocal 的应用场景主要有以下几个方面:

  • 保存线程上下文信息,在需要的地方可以获取

  • 线程间数据隔离

  • 数据库连接

6.2 说说ThreadLocal 的原理?

看了上面的源码分析,相信小伙伴都知道了。这里总结一下

ThreadLocl 的原理可以概括为下图(其中图中的虚线表示弱引用,实线为强引用):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DNkP3YnQ-1641381418238)(ThreadLocal详解.assets/image-20220105190022189.png)]

从上图可以看出每个线程都有一个ThreadLocalMap,ThreadLocalwap中保存着所有的 ThreadLocal,而ThreadLocal本身只是一个引用本身并不保存值,值都是保存在ThreadLocalMap中的,其中ThreadLocal 为ThreadLocalMap中的key。

6.3 为什么ThreadLocal 会发生内存泄漏?

因为ThreadLocal中的key是弱引用,而value是强引用。当ThreadLocal没有被强引用时,在进行垃圾回收时,key会被清理掉,而value 不会被清理掉,这时如果不做任何处理,value将永远不会被回收,产生内存泄漏。

6.4 如何解决ThreadLocal的内存泄漏?

其实在ThreadLocal在设计的时候已经考虑到了这种情况,在调用set()get()remove()等方法时就会清理掉key为null 的记录,所以在使用完ThreadLocal 后最好手动调用remove()方法。

6.5 为什么要将key设计成ThreadLocal的弱引用?

面试准备+复习分享:

为了应付面试也刷了很多的面试题与资料,现在就分享给有需要的读者朋友,资料我只截取出来一部分哦

秋招|美团java一面二面HR面面经,分享攒攒人品

sets/image-20220105190022189.png)]](https://img-blog.csdnimg.cn/52105127aff24aa0995c3611dc20b273.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBATEwuTEVCUk9O,size_20,color_FFFFFF,t_70,g_se,x_16)

从上图可以看出每个线程都有一个ThreadLocalMap,ThreadLocalwap中保存着所有的 ThreadLocal,而ThreadLocal本身只是一个引用本身并不保存值,值都是保存在ThreadLocalMap中的,其中ThreadLocal 为ThreadLocalMap中的key。

6.3 为什么ThreadLocal 会发生内存泄漏?

因为ThreadLocal中的key是弱引用,而value是强引用。当ThreadLocal没有被强引用时,在进行垃圾回收时,key会被清理掉,而value 不会被清理掉,这时如果不做任何处理,value将永远不会被回收,产生内存泄漏。

6.4 如何解决ThreadLocal的内存泄漏?

其实在ThreadLocal在设计的时候已经考虑到了这种情况,在调用set()get()remove()等方法时就会清理掉key为null 的记录,所以在使用完ThreadLocal 后最好手动调用remove()方法。

6.5 为什么要将key设计成ThreadLocal的弱引用?

面试准备+复习分享:

为了应付面试也刷了很多的面试题与资料,现在就分享给有需要的读者朋友,资料我只截取出来一部分哦

[外链图片转存中…(img-ZSnkNcYa-1714504252446)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 22
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
回答: ThreadLocal面试题主要考察对ThreadLocal的理解和使用。ThreadLocal是一种用于解决多线程并发问题的思路,它为每个线程提供了一个独立的变量副本,以避免线程之间的数据共享和竞争问题。在多线程环境下,每个线程通过ThreadLocal来访问自己的变量副本,从而保证了线程之间的数据隔离。 在面试中,可能会考到ThreadLocal的使用场景、使用方法以及其局限性。ThreadLocal的使用场景包括但不限于线程池、web应用中的用户信息存储、数据库连接管理等需要保证线程安全的情况。ThreadLocal的使用方法是通过set方法设置线程的变量副本,通过get方法获取线程的变量副本。需要注意的是,每个线程对应一个ThreadLocal实例,且每个ThreadLocal实例都会创建一个ThreadLocalMap用于存放线程的变量副本。 然而,ThreadLocal也有一些局限性。例如,ThreadLocal只能在当前线程内部共享数据,无法在线程之间共享。另外,由于ThreadLocal的使用需要创建副本,当线程数量很大时,可能会导致内存占用过大。此外,使用ThreadLocal时需要注意内存泄漏的问题,即使用完ThreadLocal后需要及时清理,否则可能会导致内存泄漏。 综上所述,ThreadLocal是一种用于解决多线程并发问题的思路,通过为每个线程提供独立的变量副本来实现线程间的数据隔离。在面试中,可能会问到ThreadLocal的使用场景、使用方法以及其局限性。需要注意的是,ThreadLocal的使用要避免内存泄漏问题,并且在多线程环境下合理使用,以确保线程安全性。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [面试题 - ThreadLocal详解](https://blog.csdn.net/wangnanwlw/article/details/108866086)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] - *3* [ThreadLocal高频面试题](https://blog.csdn.net/qq_43255017/article/details/126296062)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值