目录
前言:
当我们编写多线程程序时,经常会遇到一些需要在线程之间共享数据的情况。然而,共享数据可能会引发线程安全的问题,例如竞态条件(race condition)和数据覆盖等。为了解决这些问题,Java 提供了许多线程同步的机制,如 synchronized 关键字和 Lock 接口等。
然而,并不是所有的场景都适合使用传统的线程同步方式。有些情况下,我们更希望每个线程都拥有自己独立的数据副本,以避免线程之间的干扰。这时,ThreadLocal 就成为了一个非常有用的工具。
ThreadLocal:
ThreadLocal 是 Java 提供的一个线程本地变量工具类,它为每个线程提供了一个独立的变量副本,使得每个线程都可以独立地操作自己的变量副本,而不会影响其他线程。简单来说,ThreadLocal 可以理解为一个以线程为 key、以变量为 value 的存储结构。这种变量在线程的生命周期内起作用。
总结:
ThreadLocal可以在多线程环境下为线程创建变量副本,使得变量副本只存在于当前线程当中。而且是存在线程隔离的,每一个线程的变量副本都是相互隔离的,不会彼此影响。线程变量副本的存在缓解了线程在跨函数使用变量时的繁琐,并且可以为每一个线程单独记录数据。
举例:假设我们有一个用户管理后端,而我们在查询的时候,需要记录当前使用者的查询次数。那么就会出现一个问题:如果我们只是简单的创建一个变量记录当前查询者的查询次数的话,那么所有的使用者的查询次数都会记录到这个变量当中,无法满足我们想要的记录当前使用者的查询次数,因为在代码中创建的这个变量是被所有线程所共享的。
那么在这个时候我们就需要使用ThreadLocal。创建一个为当前线程所独享的变量。通过这种方式,我们就使得每一个用户都有了自己的一个查询计数器。
最后我们来看一看源代码中的作者是如何描述ThreadLocal的作用的:
ThreadLocal的内部结构:
早期设计:
在JDK早期版本的时候,ThreadLocal的设计方案为:每一个ThreadLocal都创建一个Map,然后用线程作为Map的Key,要存储的局部变量作为对应的value。这样就达到了各个线程的局部变量隔离的效果:
现在设计:
每一个Thread维护一个ThredLocalMap,这个Map的key是ThreadLocal本身,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<?>) this);
}
return value;
}
setInitialValue方法的整体逻辑为:获取到value值(这里使用的initialValue默认返回null),获取到当前线程。之后再根据当前线程查询是否有对应的map。如果当前线程有对应的map,那么就更新值。否则的话就进行创建。
最后instance of 关键字用来判断this的类型是否属于TerminatingThreadLocal。如果this属于是erminatingThreadLocal类型的,那么就调用register方法将this进行注册到TerminatingThreadLocal类中。
总结来说,最后的这段代码用于将终止类型的 ThreadLocal 实例注册到 TerminatingThreadLocal 类的静态列表中。这样,在线程退出时,终止类型的 ThreadLocal 实例会自动从 ThreadLocalMap 中移除,避免内存泄漏。
当一个线程终止时,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子类,以便在子线程中继承父线程的数据。
如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!
版权声明:本文为博主作者:我是一盘牛肉原创文章,版权归属原作者,如果侵权,请联系我们删除!