ThreadLocal

tim-qtp...大约 5 分钟多线程

🌟ThreadLocal 是什么?

ThreadLocal 是一种用于实现线程局部变量的工具类。它允许每个线程都拥有自己的独立副本,从而实现线程隔离。

ThreadLocal线程副本
ThreadLocal线程副本

使用 ThreadLocal 通常分为四步:

①、创建 ThreadLocal

//创建一个ThreadLocal变量
public static ThreadLocal<String> localVariable = new ThreadLocal<>();

②、设置 ThreadLocal 的值

//设置ThreadLocal变量的值
localVariable.set("沉默王二是沙雕");

③、获取 ThreadLocal 的值

//获取ThreadLocal变量的值
String value = localVariable.get();

④、删除 ThreadLocal 的值

//删除ThreadLocal变量的值
localVariable.remove();

在 Web 应用中,可以使用 ThreadLocal 存储用户会话信息,这样每个线程在处理用户请求时都能方便地访问当前用户的会话信息。

在数据库操作中,可以使用 ThreadLocal 存储数据库连接对象,每个线程有自己独立的数据库连接,从而避免了多线程竞争同一数据库连接的问题。

在格式化操作中,例如日期格式化,可以使用 ThreadLocal 存储 SimpleDateFormat 实例,避免多线程共享同一实例导致的线程安全问题。

ThreadLocal 有哪些优点?

每个线程访问的变量副本都是独立的,避免了共享变量引起的线程安全问题。由于 ThreadLocal 实现了变量的线程独占,使得变量不需要同步处理,因此能够避免资源竞争。

ThreadLocal 可用于跨方法、跨类时传递上下文数据,不需要在方法间传递参数。

实际项目中用到的 ThreadLocal ?

用来存储用户信息,登录后的用户每次访问接口,都会在请求头中携带一个 token,在控制层可以根据这个 token,解析出用户的基本信息。

假如在服务层和持久层也要用到用户信息,就可以在控制层拦截请求把用户信息存入 ThreadLocal。

这样我们在任何一个地方,都可以取出 ThreadLocal 中存的用户信息

很多其它场景的 cookie、session 等等数据隔离都可以通过 ThreadLocal 去实现。

ThreadLoca存放用户上下文
ThreadLoca存放用户上下文

ThreadLocal 怎么实现的呢?

当我们创建一个 ThreadLocal 对象并调用 set 方法时,其实是在当前线程中初始化了一个 ThreadLocalMap。

ThreadLocalMap
ThreadLocalMap

ThreadLocalMap 是 ThreadLocal 的一个静态内部类,它内部维护了一个 Entry 数组,key 是 ThreadLocal 对象,value 是线程的局部变量,这样就相当于为每个线程维护了一个变量副本。

ThreadLoca结构图
ThreadLoca结构图

Entry 继承了 WeakReference,它限定了 key 是一个弱引用,弱引用的好处是当内存不足时,JVM 会回收 ThreadLocal 对象,并且将其对应的 Entry.value 设置为 null,这样可以在很大程度上避免内存泄漏。

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    //节点类
    Entry(ThreadLocal<?> k, Object v) {
        //key赋值
        super(k);
        //value赋值
        value = v;
    }
}

总结一下:

ThreadLocal 的实现原理是,每个线程维护一个 Map,key 为 ThreadLocal 对象,value 为想要实现线程隔离的变量(对象)。

1、通过 ThreadLocal 的 set 方法将对象存入 Map 中。

2、通过 ThreadLocal 的 get 方法从 Map 中取出对象。

3、Map 的大小由 ThreadLocal 对象的多少决定。

什么是弱引用,什么是强引用?

先说一下强引用,比如 User user = new User("秦一") 中,user 就是一个强引用,new User("秦一") 就是强引用对象。

当 user 被置为 null 时(user = null),new User("秦一") 对象就会被垃圾回收;否则即便是内存空间不足,JVM 也不会回收 new User("秦一") 这个强引用对象,宁愿抛出 OutOfMemoryError。

弱引用,比如说在使用 ThreadLocal 中,Entry 的 key 就是一个弱引用对象。

ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
userThreadLocal.set(new User("沉默王二"));

userThreadLocal 是一个强引用,new ThreadLocal<>() 是一个强引用对象;

new User("秦一") 是一个强引用对象。

调用 set 方法后,会将 key = new ThreadLocal<>() 放入 ThreadLocalMap 中,此时的 key 是一个弱引用对象。当 JVM 进行垃圾回收时,如果发现了弱引用对象,就会将其回收。

为什么使用弱引用?

ThreadLocal 中使用弱引用的原因是:

  • 避免内存泄漏:如果 ThreadLocal 对象是强引用,那么线程结束时,ThreadLocalMap 中的条目可能不会被清理,导致内存泄漏。
  • 自动清理:使用弱引用可以让 JVM 在回收 ThreadLocal 对象时,自动清理对应的条目,减少内存泄漏的风险。

父线程能用 ThreadLocal 给子线程传值吗?

不能。

因为 ThreadLocal 变量存储在每个线程的 ThreadLocalMap 中,而子线程不会继承父线程的 ThreadLocalMap

可以使用 InheritableThreadLocal来解决这个问题。

子线程在创建的时候会拷贝父线程的 InheritableThreadLocal 变量。

class InheritableThreadLocalExample {
    private static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {
        inheritableThreadLocal.set("父线程的值");

        new Thread(() -> {
            System.out.println("子线程获取的值:" + inheritableThreadLocal.get()); // 继承了父线程的值
        }).start();
    }
}

InheritableThreadLocal的原理

在 Thread 类的定义中,每个线程都有两个 ThreadLocalMap:

public class Thread {
    /* 普通 ThreadLocal 变量存储的地方 */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /* InheritableThreadLocal 变量存储的地方 */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

普通 ThreadLocal 变量存储在 threadLocals 中,不会被子线程继承。

InheritableThreadLocal 变量存储在 inheritableThreadLocals 中,当 new Thread() 创建一个子线程时,Thread 的 init() 方法会检查父线程是否有 inheritableThreadLocals,如果有,就会拷贝 InheritableThreadLocal 变量到子线程: