为什么出现ThreadLocal ?
在多线程环境下,如果多个线程同时修改一个公共变量,可能会出现线程安全问题,即该变量的最终结果可能出现异常。为了解决线程安全问题,JDK提供了很多技术手段,比如使用synchronized或Lock来给访问公共资源的代码上锁,保证了代码的原子性。但在高并发的场景中,如果多个线程同时竞争一把锁,这时会存在大量的锁等待,可能会浪费很多时间,让系统的响应时间变慢。因此,JDK还提供了另外一种用空间换时间的新思路:ThreadLocal。它的核心思想是:共享变量在每个线程都有一个副本,每个线程操作的都是自己的副本,对另外的线程没有影响。
ThreadLocal是什么?
ThreadLocal是一个与线程相关的类,但它本身并不是一个Thread。
这个类可以提供线程局部变量,与普通变量有所不同。虽然你可以实例化一个ThreadLocal对象,但当每个线程访问或设置它时,它们实际上是在操作本线程内的该对象的副本。这也意味着,这个对象在不同的线程中,副本的值是不一样的
ThreadLocal是Java提供的一个线程本地变量的实现机制,其核心目的是让每个线程拥有自己独立的变量副本。这样,在多线程环境下,每个线程都可以独立地改变自己的副本,而不会影响其他线程的副本,从而实现了线程数据隔离,避免了线程安全问题。
如何使用?
public class ThreadLocalExample {
private static ThreadLocal<String> threadLocalVar = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// 设置线程1中的本地变量值
threadLocalVar.set("Hello");
// 打印本地变量
System.out.println("Thread 1 value: " + threadLocalVar.get());
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// 设置线程2中的本地变量值
threadLocalVar.set("World");
// 打印本地变量
System.out.println("Thread 2 value: " + threadLocalVar.get());
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
我们创建了两个线程(t1和t2),每个线程都设置了不同的本地变量值,并打印出来。通过使用ThreadLocal类,每个线程都有自己独立的本地变量副本,因此它们之间不会相互干扰。
输出结果
Thread 1 value: Hello
Thread 2 value: World
注意:
- ThreadLocal 和集合类一样,在创建时需要指定类型,上如图就指定的 String 类型
- ThreadLocal 的读写和设置不是用的等于号 , 而是要使用该ThreadLocal 的 set 和 get 方法
ThreadLocal如何实现?
public class ThreadLocal<T> {
public T get() {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
// 根据当前ThreadLocal对象获取对应的Entry对象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
// 返回Entry对象的值作为结果
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果map为空或者Entry对象为空,则设置初始值并返回
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
// 返回当前线程的threadLocals成员变量
return t.threadLocals;
}
public void set(T value) {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
// 将给定的ThreadLocal对象和对应的值存储到table中
map.set(this, value);
} else {
// 如果map为空,则创建一个新的ThreadLocalMap对象并存储给定的值
createMap(t, value);
}
}
ThreadLocalMap getMap(Thread t) {
// 返回当前线程的threadLocals成员变量
return t.threadLocals;
}
}
public class Thread implements Runnable {
//...
// 定义一个ThreadLocalMap类型的成员变量threadLocals,用于存储当前线程的ThreadLocal对象及其对应的值
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
public class ThreadLocal<T> {
static class ThreadLocalMap {
private Entry[] table; // 存储键值对的Entry数组
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // 使用super()方法创建弱引用,key值为ThreadLocal对象
value = v;
}
}
//...
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1); // 计算索引位置
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e); // 如果未找到,则进行查找操作
}
private void set(ThreadLocal<?> key, Object value) {
// 将给定的ThreadLocal对象和对应的值存储到table中
}
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;
}
}
}
}
}
ThreadLocalMap 本质是一个<key,value>形式的节点组成的数组,
ThreadLocalMap 的节点是一个静态内部类 Entry
,它继承自 WeakReference<ThreadLocal<?>>
。这意味着每个 Entry
对象都是一个弱引用,其引用的是 ThreadLocal
类型的对象作为键(key)。值为代码放入的值
弱引用
- 不阻断垃圾回收:与强引用不同,弱引用不会增加对象的引用计数,因此即使有弱引用指向一个对象,该对象也可能在任何时间被垃圾回收器回收。
- 生命周期短暂:一旦垃圾回收器发现某个对象只被弱引用指向,不管当前内存是否足够,这个对象都会被回收。
图源 https://javabetter.cn/sidebar/sanfene/javase.html
看张经典的图帮助我们理解
图源来自阿里文档
ThreadLocal的实现原理可以分为两个关键部分:
-
每个线程(Thread)内部都有一个独立的Map对象,用于存储数据。由于每个线程都有自己的Map,因此不同线程之间的数据是隔离的,不会互相干扰。
-
当我们创建一个ThreadLocal对象时,这个对象将作为键(key)来帮助我们在线程内部的Map中定位数据。
下面是对ThreadLocal的核心机制的简要概括:
- 我们创建的ThreadLocal对象仅仅是一个键(key)。
- 每个线程(Thread)内部都有一个Map,这个Map中存储了ThreadLocal对象(作为键)和该线程为这个ThreadLocal对象设置的值(作为值)。
- 对于不同的线程来说,每次获取自己的副本值时,其他线程无法访问到当前线程的副本值,从而实现了副本之间的隔离,互不干扰。
ThreadLocal的内存泄漏问题
ThreadLocal可能会引起内存泄漏问题
首先,了解内存泄漏的基础知识是必要的。内存泄漏是指一块被分配的内存既不能被访问,也无法被垃圾回收器回收的情况。在Java中,强引用与弱引用对垃圾回收的影响不同,强引用会阻止对象被垃圾回收,而弱引用则不会。
当ThreadLocal
对象(作为ThreadLocalMap
中的键)被垃圾回收器回收,而与之关联的ThreadLocalMap
仍然存活时(由于其生命周期通常与线程相同),如果对应的值没有被适当地清理,就会出现以下情况:
ThreadLocalMap
中仍然存在键(ThreadLocal
对象)为null
的条目。- 这些条目的值(即
ThreadLocal
对象所关联的数据)依然占用内存。 - 由于
ThreadLocal
对象已被回收,这些值无法通过正常的引用路径被访问或清理。
这种情况就会导致内存泄漏问题,因为即使ThreadLocal
对象不再需要,其对应的值也无法被垃圾回收,从而占用不必要的内存资源。
为了避免这种内存泄漏,可以采取以下措施:
-
显式清理:在使用完
ThreadLocal
后,调用remove()
方法来显式地从ThreadLocalMap
中移除对应的键值对。 -
合理设计:在设计上,确保
ThreadLocal
对象的生命周期与使用它的线程相匹配,避免长时间持有不必要的ThreadLocal
对象。 -
线程池管理:在使用线程池时,注意线程的复用,及时清理线程本地变量,特别是在长生命周期的线程池中。
-
资源监控:定期监控和分析应用的内存使用情况,以便及时发现潜在的内存泄漏问题。
那为什么还要设计为弱引用?
在现代程序运行中,线程池模式被广泛采用,这导致线程的生命周期变得相对较长。由于ThreadLocal可以为每个线程提供独立的数据存储,如果这些数据没有得到及时回收,就可能导致内存泄漏和最终的内存溢出(OOM)。
为了减轻程序员手动管理资源的负担,并防止内存溢出的风险,Java引入了一套基于弱引用的自动回收机制。这个机制的核心是使用WeakReference
来包装ThreadLocal对象,从而允许垃圾回收器在需要时回收它们。
下面是对这种机制的简要概括:
- 弱引用包装:ThreadLocal对象被封装在一个弱引用中,这样即使它被用作ThreadLocalMap的键,也不会阻止垃圾回收器回收它。
- 自动清理:当ThreadLocal对象不再强引用时,垃圾回收器可以自动回收它,从而避免内存泄漏。
- 资源管理:通过这种方式,程序员不需要显式地调用remove方法来清理ThreadLocal对象,减少了手动管理资源的复杂性。
- 防止OOM:这种弱引用机制有助于减少长时间运行的线程池中ThreadLocal对象的累积,防止内存溢出。
参考文献:
图解,深入浅出带你理解ThreadLocal-阿里云开发者社区 https://javabetter.cn/sidebar/sanfene/javase.html