目录

前言:

ThreadLocal:

ThreadLocal的内部结构:

ThreadLocal的常用方法:

1.set方法:

2.get方法:

3.setInitialValue方法

remove方法():

ThreadLocalMap:​编辑

成员变量:

存储结构:

高频考点:

总结:


前言:

当我们编写多线程程序时,经常会遇到一些需要在线程之间共享数据的情况。然而,共享数据可能会引发线程安全的问题,例如竞态条件(race condition)和数据覆盖等。为了解决这些问题,Java 提供了许多线程同步的机制,如 synchronized 关键字和 Lock 接口等。

然而,并不是所有的场景都适合使用传统的线程同步方式。有些情况下,我们更希望每个线程都拥有自己独立的数据副本,以避免线程之间的干扰。这时,ThreadLocal 就成为了一个非常有用的工具。

ThreadLocal:

ThreadLocal 是 Java 提供的一个线程本地变量工具类,它为每个线程提供了一个独立的变量副本,使得每个线程都可以独立地操作自己的变量副本,而不会影响其他线程。简单来说,ThreadLocal 可以理解为一个以线程为 key、以变量为 value 的存储结构。这种变量在线程的生命周期内起作用。

总结:

ThreadLocal可以在多线程环境下为线程创建变量副本,使得变量副本只存在于当前线程当中。而且是存在线程隔离的,每一个线程的变量副本都是相互隔离的,不会彼此影响。线程变量副本的存在缓解了线程在跨函数使用变量时的繁琐,并且可以为每一个线程单独记录数据

举例:假设我们有一个用户管理后端,而我们在查询的时候,需要记录当前使用者的查询次数。那么就会出现一个问题:如果我们只是简单的创建一个变量记录当前查询者的查询次数的话,那么所有的使用者的查询次数都会记录到这个变量当中,无法满足我们想要的记录当前使用者的查询次数,因为在代码中创建的这个变量是被所有线程所共享的。

那么在这个时候我们就需要使用ThreadLocal。创建一个为当前线程所独享的变量。通过这种方式,我们就使得每一个用户都有了自己的一个查询计数器。

最后我们来看一看源代码中的作者是如何描述ThreadLocal的作用的:

ThreadLocal的内部结构:

早期设计:

在JDK早期版本的时候,ThreadLocal的设计方案为:每一个ThreadLocal都创建一个Map,然后用线程作为MapKey,要存储的局部变量作为对应的value。这样就达到了各个线程的局部变量隔离的效果:

现在设计:

每一个Thread维护一个ThredLocalMap,这个MapkeyThreadLocal本身,value才是真正要存储的值。

换句话来说:

(1)每一个Thread线程内部都有一个Map

(2)Map里面存储ThreadLocal对象(Key)线程的变量副本(Vlaue)

(3)Thread内部的map是由ThreadLLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。

ThreadLocal的常用方法:

方法声明描述
ThreadLocal()创建ThreadLocal对象
set()设置当前线程的变量副本
get()获取当前线程的变量副本
remove()移除当前线程的变量副本

1.set方法:

public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {map.set(this, value);} else {createMap(t, value);}}

set方法的整体逻辑为:先获取到当前线程,再根据当前线程获取到对应的ThreadLocalMap,如果map不为空就进行插入,如果map为空的话就创建一个map并且插入t和value。

2.get方法:

public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();}

get方法的整体逻辑为:先获取到当前线程,再根据当前线程获取到对应的ThreadLocalMap。如果map不为空的话,就根据当前的ThreadLocal为Key,调用getEntry获取到对应的存储实体e,如果e不为空的话,就获取到e所对应的value值。如果获取到的map为空的话,就执行初始化。

3.setInitialValue方法

private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {map.set(this, value);} else {createMap(t, value);}if (this instanceof TerminatingThreadLocal) {TerminatingThreadLocal.register((TerminatingThreadLocal当一个线程终止时,JVM会自动调用Thread类中的ThreadLocal.ThreadLocalMap.threadTerminated()方法,该方法会遍历所有的ThreadLocal对象,并调用TerminatingThreadLocal类的terminate()方法。terminate()方法将删除该线程所有使用的TerminatingThreadLocal对象所对应的本地变量,从而释放内存。这种方式比较安全和可靠,因为即使用户没有手动清理线程本地变量,也不会造成内存泄漏。

remove方法():

 public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) { m.remove(this); } }

它调用了threadmap的remove方法,因此我们转到这个方法里面:

private void remove(ThreadLocal key) {Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {if (e.get() == key) {e.clear();expungeStaleEntry(i);return;}}}

remove方法的整体逻辑为:根据ThreadLocal对象的哈希码,在ThreadLocalMap中定位并移除对应的Entry对象,以实现对ThreadLocal对象的移除操作。

其实通过这么多的源码分析我们能够看出:想要深入了解ThreadLocal,就一定要深入了解ThreadLocalMap。因此我们来介绍一下ThreadLocalMap

ThreadLocalMap:

部分源码截取:

由此我们可以看出:ThreadLocalMap并没有实现Map接口。

需要注意的是:ThreadLocalMap虽然是ThreadLocal的内部类,但是其实例却是由Thread对象维护的。

成员变量:

/** * 初始容量 -- 必须是2的整次幂. */private static final int INITIAL_CAPACITY = 16;/** * The table, resized as necessary. * table.length MUST always be a power of two. */private Entry[] table;/** * 当前表中enrtys的个数. */private int size = 0;/** * 扩容阈值. */private int threshold; // Default to 0

在这里数组长度也必须是二的n次幂。道理和HashMap篇讲的一样:

我们在使用remove方法的时候使用了哈希码和数组长度-1进行&运算。本来是哈希码%数组长度。但是为了提高效率我们采用位运算,如果想要哈希码和数组长度-1进行&运算的结果和哈希码%数组的结果一致,我们就需要让数组的长度等于二的n次幂。

更详细的解释可以看我HashMap的文章:

【从零开始学习JAVA集合 | 第一篇】深入解读HashMap源码(含面试题)-CSDN博客https://liyuanxin.blog.csdn.net/article/details/134867511

存储结构:

 Entry(ThreadLocal k, Object v) {super(k);value = v;}

高频考点:

1.ThreadLocal为什么会出现内存泄漏?

当Thread是线程池中的一个对象的时候就会发生内存泄漏。这是因为当ThreadLocal对象使用完之后,本来应该要对Entry对象进行回收。但是线程池中的线程不会被销毁,线程对象是通过强引用指向ThreadLocalMap的,ThreadLocalMap也是通过强引用指向Entry对象的,因此线程不被回收。Entry对象也不会被回收,这就出现了内存泄漏。

基于这种不稳定性,我们设置Entry中的Key(ThreadLocal)为弱引用,在下一次垃圾回收的时候回收掉这个Key。此时Key就为null了,而ThreadLocalMap的set/get/remove在检测到key==null的时候,会自动的把value清空,这样就实现了避免内存泄漏。

2.ThreadLocal的使用场景?

1.Java中有的对象在被多个线程同时操作的时候就会报错,例如SimpleDateFormat对象。这个时候我们就可以使用ThreadLocal来为每一个线程都创建一个ThreadLocal对象。

2.全局存储用户信息,比如用户ID。我们就可以将其放到ThreadLocal对象当中,被线程独享。

其实我认为:ThreadLocal的作用不是去保护共享数据的安全性,而是去做数据的隔离。因此他和加锁关键字synchronized 并没有什么可比的。因为synchronized是做共享数据的安全性的。

总结:

总的来说,ThreadLocal是Java中的一个线程局部变量,它为每个线程提供了一个独立的变量副本,保证了线程安全。ThreadLocal可以用于管理线程私有的数据,避免竞争条件,提高系统的并发性能。在实际应用中,ThreadLocal常用于数据库连接管理、用户登录信息管理、线程调试信息传递、消息传递等场景。

需要注意的是,虽然ThreadLocal可以有效地解决线程安全问题,但是过度使用ThreadLocal也可能会导致内存泄漏问题。因此,在使用ThreadLocal时,需要特别注意及时清理ThreadLocal中的数据,避免不必要的内存占用。

此外,还需要注意ThreadLocal的使用方式,尽量避免在多个线程之间共享ThreadLocal实例,以免出现意外的问题。同时,也应该考虑使用InheritableThreadLocal子类,以便在子线程中继承父线程的数据。

如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!