深入了解 Java 中的 ThreadLocal

ThreadLocal 是 Java 中一个有趣且强大的工具,它允许我们在多线程环境下为每个线程保留独立的变量副本。这种机制使得我们能够在不同线程中安全地使用类似全局变量的对象,而不必担心线程安全性的问题。在本文中,我们将深入探讨 ThreadLocal 的用法、原理和一些最佳实践。

1. ThreadLocal 的基本用法

ThreadLocal 提供了一种将变量与线程关联起来的方式,确保每个线程都可以拥有自己独立的变量。其基本用法可以分为以下几步:

1.1 创建 ThreadLocal 对象

ini 复制代码
ThreadLocal<String> threadLocal = new ThreadLocal<>();

1.2 设置线程局部变量的值

bash 复制代码
threadLocal.set("Hello, ThreadLocal!");

1.3 获取线程局部变量的值

ini 复制代码
String value = threadLocal.get();
System.out.println(value); // 输出: Hello, ThreadLocal!

1.4 移除线程局部变量

java 复制代码
threadLocal.remove();

1.5 ThreadLocal 多线程 示例

java 复制代码
public class Test {
    static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        // 创建并启动两个线程
        Thread thread1 = new Thread(() -> {
            // 在线程1中设置局部变量的值
            threadLocal.set("Value set in Thread 1");
            // 调用方法,演示在同一线程中获取局部变量的值
            printThreadLocalValue();
        });
        Thread thread2 = new Thread(() -> {
            // 在线程2中设置局部变量的值
            threadLocal.set("Value set in Thread 2");
            // 调用方法,演示在同一线程中获取局部变量的值
            printThreadLocalValue();
        });
        // 启动两个线程
        thread1.start();
        thread2.start();
        // 等待两个线程执行完毕
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 在主线程中获取局部变量的值,此时应该为 null
        printThreadLocalValue();
    }

    private static void printThreadLocalValue() {
        // 获取当前线程的局部变量的值
        String value = threadLocal.get();
        // 打印线程名称和局部变量的值
        System.out.println(Thread.currentThread().getName() + ": " + value);
        // 清除本地内存中的本地变量
        threadLocal.remove();
        // 打印线程名称和移除后的本地变量
        System.out.println(Thread.currentThread().getName() + ":after remove: " + threadLocal.get());

    }
}

输出结果

yaml 复制代码
Thread-0: Value set in Thread 1
Thread-1: Value set in Thread 2
Thread-0:after remove: null
Thread-1:after remove: null
main: null
main:after remove: null

在这个示例中,我们创建了一个 ThreadLocal 对象,用于保存每个线程的局部变量。然后,我们创建了两个线程,分别设置了不同的局部变量值,并调用了一个方法 printThreadLocalValue() 来获取并打印局部变量的值。最后,我们等待两个线程执行完毕,并在主线程中再次获取局部变量的值。

由于每个线程都有自己独立的局部变量副本,因此在不同线程中设置的值不会相互影响。在主线程中获取局部变量的值时,由于主线程没有设置过局部变量,因此输出为 null

2. ThreadLocal 的原理

ThreadLocal 的实现原理涉及到一个名为 ThreadLocalMap 的类。每个线程都维护着个 ThreadLocalMap,它是一个自定义的哈希表,用于存储线程本地变量。

java 复制代码
public class Thread implements Runnable {
    ...
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    ...
}

当我们调用 ThreadLocal 的 set 方法时,实际上是在当前线程的 ThreadLocalMap 中存储了一个键值对,其中 ThreadLocal 对象本身 是我们 设置的变量

java 复制代码
public class ThreadLocal<T> {
    ...
    public void set(T value) {
        // 获取当前执行该方法的线程对象
        Thread t = Thread.currentThread();
        // 调用 getMap(t) 方法获取当前线程的 ThreadLocalMap 对象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }
   
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    // 用于在当前线程上创建与 `ThreadLocal` 关联的映射表。
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

    ...
}

同样,调用 get 方法时,会根据 当前线程获取到对应的 ThreadLocalMap ,然后 根据 ThreadLocal 对象查找相应的值

java 复制代码
public class ThreadLocal<T> {
    ...
    /**
    * 返回当前线程本地副本中此线程本地变量的值。如果对于当前线程,该变量没有值,
    * 首先将其初始化为通过调用 initialValue 方法返回的值。
    */
    public T get() {
        // 获取当前线程的引用
        Thread t = Thread.currentThread();
        // 获取当前线程上的映射表,该映射表存储了所有与 ThreadLocal 关联的变量及其对应的值
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 从映射表中获取与当前 ThreadLocal 对象关联的条目
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                // 如果找到了条目,说明当前线程已经设置过这个 ThreadLocal 变量的值,直接返回该值
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 如果映射表中没有找到与当前 ThreadLocal 关联的条目,调用 setInitialValue() 方法进行初始化,并返回初始值
        return setInitialValue();
    }
    ...
}

由于每个线程都有自己的 ThreadLocalMap,所以不同线程之间的变量不会发生冲突,彼此独立。

3. ThreadLocal 的应用场景

3.1 用户身份信息传递

如:在每次前端请求后端接口的时候,进行统一拦截,拦截后设置用户信息到 ThreadLocal。 执行业务逻辑时,无需传递用户信息参数。

java 复制代码
public class UserContext {

    private static final ThreadLocal<User> userThreadLocal = new ThreadLocal<>();

    public static void setUser(User user) {
        userThreadLocal.set(user);
    }

    public static User getUser() {
        return userThreadLocal.get();
    }

    public static void clearUser() {
        userThreadLocal.remove();
    }
}

4. ThreadLocal 的注意事项和最佳实践

在使用 ThreadLocal 时,我们需要注意一些事项以及遵循一些建议的最佳实践:

4.1 内存泄漏

4.1.1 内存泄漏原因

内存泄漏和ThreadLocalMap中定义的Entry类有非常大的关系。

java 复制代码
static class ThreadLocalMap {
   
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    .....
}

Entry将ThreadLocal作为Key,值作为Value保存,它继承自WeakReference,注意构造函数里的第一行代码super(k),这意味着ThreadLocal对象是一个「弱引用」。有的小伙伴可能对「弱引用」不太熟悉,这里再介绍一下Java的四种引用关系。
强引用 :只要强引用关系还在,对象就永远不会被回收
软引用 :在内存溢出前对其进行回收
弱引用 :不管内存是否够用,下次GC一定回收
虚引用:等同于没有引用,回收时会收到一个系统通知

由于ThreadLocal对象是弱引用,如果外部没有强引用指向它,它就会被GC回收,导致Entry的Key为空(null),如果这时Value外部也没有强引用指向它,那么Value就永远也访问不到了,按理也应该被GC回收,但是由于Entry对象还在强引用Value,导致Value无法被回收,这时「内存泄漏」就发生了,Value成了一个永远也无法被访问,但是又无法被回收的对象。

造成内存泄漏的原因总结

threadLocals对象中的Entry对象不再使用后,如果没有及时清除Entry对象 ,而程序自身也无法通过垃圾回收机制自动清除 就可能导致内存泄漏

4.1.2 如何避免内存泄漏

  1. 每次使用完ThreadLocal都记得调用remove()方法清除数据。
  2. 将ThreadLocal变量尽可能地定义成static final,避免频繁创建ThreadLocal实例。这样也就保证程序一直存在ThreadLocal的强引用,也能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的Value值,进而清除掉。

4.2 初始值设定

可以使用 ThreadLocalwithInitial 方法设置初始值。这样可以避免在 get 方法时需要进行空值判断。 如果不设定初始值,当线程首次访问 ThreadLocal 变量时,如果没有先调用 set 方法为其赋值,那么默认情况下它会返回 null。这样可能导致在使用这个变量时出现空指针异常。通过设定初始值,可以避免这种情况,确保变量总是有一个非空的默认值。

空指针异常例子

java 复制代码
private static ThreadLocal<String> myThreadLocal = new ThreadLocal<>();

public static void main(String[] args) {
    new Thread(() -> {
        String value = myThreadLocal.get();
        // 此时 value 为 null,如果不做空值判断,可能导致空指针异常
        System.out.println(value.length());
    }).start();
}

4.3 避免过多使用

虽然 ThreadLocal 提供了一种在多线程环境下共享变量的机制,但并不是适用于所有情况。过度使用 ThreadLocal 可能会导致代码的可读性变差,因此需要谨慎使用。

5. 结语

ThreadLocal 是 Java 并发编程中的一项强大工具,通过为每个线程提供独立的变量副本,有效解决了多线程环境下共享变量可能引发的线程安全性问题。在使用 ThreadLocal 时,我们需要注意内存泄漏和初始值设定等问题,以确保其正确而高效地发挥作用。通过合理使用 ThreadLocal,我们能够更好地设计多线程应用,提高代码的可维护性和性能。

相关推荐
毕设源码-赖学姐9 分钟前
【开题答辩全过程】以 基于Springboot的智慧养老系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
jamesge201011 分钟前
限流之漏桶算法
java·开发语言·算法
jvstar12 分钟前
JAVA面试题和答案
java
冷雨夜中漫步12 分钟前
OpenAPITools使用——FAQ
android·java·缓存
9坐会得自创17 分钟前
使用marked将markdown渲染成HTML的基本操作
java·前端·html
Hello.Reader38 分钟前
Flink ML 线性 SVM(Linear SVC)入门输入输出列、训练参数与 Java 示例解读
java·支持向量机·flink
oioihoii39 分钟前
C++数据竞争与无锁编程
java·开发语言·c++
最贪吃的虎39 分钟前
什么是开源?小白如何快速学会开源协作流程并参与项目
java·前端·后端·开源
资生算法程序员_畅想家_剑魔40 分钟前
Java常见技术分享-16-多线程安全-并发编程的核心问题
java·开发语言
We....40 分钟前
Java SPI 机制
java·开发语言