# threadlocal

# 1. ThreadLocal 介绍

# 1.1. 官网介绍

从 Java 官方文档中的描述: ThreadLocal 类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过 get/set 方法访问)时能保证各个线程的变量相对独立于其它线程内的变量。ThreadLocal 实例通常来说都是 private static 类型的,用于关联线程和线程上下文。

我们可以得知 ThreadLocal 的作用是: 提供线程内的局部变量,不同的线程之间不会互相干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。

总结:

  1. 线程并发
    • 在多线程并发的场景下
  2. 传递数据
    • 通过 ThreadLocal 在同一线程、不同组件中传递公共变量
  3. 线程隔离
    • 每个线程的变量都是独立的,不会互相影响

# 1.2. 基本使用

# 1.2.1. 常用方法

方法声明 描述
ThreadLocal() 创建 ThreadLocal 对象
public void set(T value) 设置 当前线程绑定的局部变量
public T get() 获取 当前线程绑定的局部变量
public void remove() 移除 当前线程绑定的局部变量

# 1.2.2. 使用案例

多线程 读写共享变量 出问题:

public class MyDemo01 {
    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args) {
        MyDemo01 instance = new MyDemo01();

        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(() -> {
                String currentThreadName = Thread.currentThread().getName();

                // 给 content 属性设置值
                instance.setContent(currentThreadName + "的数据");

                // 打印 content 属性的值
                System.out.println(currentThreadName + " => " + instance.getContent());
            });

            thread.setName("线程" + i);
            thread.start();
        }

        /* =>
        线程3 => 线程3的数据
        线程0 => 线程1的数据
        线程4 => 线程1的数据
        线程2 => 线程1的数据
        线程1 => 线程1的数据
         */
    }
}

使用 ThreadLocal 解决上述问题:

public class MyDemo02 {
    private final ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public String getContent() {
        return threadLocal.get();
    }

    public void setContent(String content) {
        threadLocal.set(content);
    }

    public static void main(String[] args) {
        MyDemo02 instance = new MyDemo02();

        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(() -> {
                String currentThreadName = Thread.currentThread().getName();

                // 给 content 属性设置值
                instance.setContent(currentThreadName + "的数据");

                // 打印 content 属性的值
                System.out.println(currentThreadName + " => " + instance.getContent());
            });

            thread.setName("线程" + i);
            thread.start();
        }
    }
}

# 1.3. ThreadLocal 类 与 synchronized 关键字

虽然 ThreadLocal 与 synchronized 关键字都能处理多线程并发访问变量的问题,不过两者处理问题的角度和思路不同

- synchronized ThreadLocal
原理 同步机制采用“以时间换空间”的方式,只提供了一份变量,让不同的线程排队访问 ThreadLocal 采用“以空间换时间”的方式,为每一个线程都提供了一份变量的副本,从而实现同时访问而互不干扰
侧重点 多个线程之间访问资源的同步 多线程中让每个线程之间的数据相互隔离

# 2. 应用场景

# 2.1. 转账

转账案例:

  • 涉及 service 、dao 层的方法,都要获取数据库连接来读写数据
  • 可以将 Connection 对象保存在 ThreadLocal

优势:

  • 传递数据
    • 保存每个线程绑定的数据,在需要的地方可以直接获取,避免参数直接传递带来的代码耦合问题
  • 线程隔离
    • 各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失

# 2.2. 在当前线程保存 user 对象

@Data
public class UserDTO {
    private Long id;
    private String nickName;
    private String icon;
}


public class UserHolder {
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();

    public static void saveUser(UserDTO user){
        tl.set(user);
    }

    public static UserDTO getUser(){
        return tl.get();
    }

    public static void removeUser(){
        tl.remove();
    }
}


@Slf4j
public class RefreshTokenInterceptor implements HandlerInterceptor {
    private final StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 获取请求头中 token
        String token = request.getHeader("authorization");

        // 1.1 不存在,直接放行 并 返回
        if (StrUtil.isEmpty(token)) {
            // log.info("token 为空");
            return true;
        }

        // 2. 基于 token 获取 Redis 中存储的 user
        String tokenKey = RedisConstants.LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);

        // 3. 用户不存在,直接放行 并 返回
        if (CollectionUtil.isEmpty(userMap)) {
            log.info("用户不存在");
            return true;
        }

        // 4. 用户存在,存入 ThreadLocal
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        UserHolder.saveUser(userDTO);

        // 5. 刷新 token 有效期
        stringRedisTemplate.expire(tokenKey, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);

        // 6. 放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

# 3. ThreadLocal 的内部结构

在 JDK8 中 ThreadLocal 的设计是:

  • 每个 Thread 都维护一个 ThreadLocalMap
  • Map 的 key 是 ThreadLocal 实例本身
  • Map 的 value 是真正要存储的值

具体的过程:

  1. 每个 Thread 线程内部都有一个 Map (ThreadLocalMap)
  2. Map 里面存储 ThreadLocal 对象(key) 和 线程的变量副本(value)
  3. Thread 内部的 Map 是由 ThreadLocal 维护,由 ThreadLocal 负责向 map 获取和设置 线程的变量值
  4. 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰

# 4. ThreadLocalMap

ThreadLocalMap 是 ThreadLocal 的内部类,没有实现 Map 接口,用独立的方式实现了 Map 的功能,其内部的 Entry 也是独立实现的

在 ThreadLocalMap 中,也是用 Entry 来保存 key-value 结构数据的。不过 Entry 中的 key 只能是 ThreadLocal对象

Entry 继承 WeakReference,key(ThreadLocal 对象) 是弱引用,其目的是将 ThreadLocal 对象的生命周期和线程生命周期解绑。

# 4.1. 弱引用和内存泄漏

内存泄漏相关概念:

  • Memory overflow: 内存溢出,没有足够的内存提供申请者使用
  • Memory leak: 内存泄漏是指程序中已动态分配的堆内存由于某种原因导致程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果,内存泄漏的堆积终将导致内存溢出。

弱引用相关概念:

  • Java 中的引用类型有 4 种: 强、软、弱、虚
  • 强引用(Strong Reference),就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就表明该对象还“活着”,垃圾回收器就不会回收这种对象
  • 弱引用(WeakReference),垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存

ThreadLocal 内存泄漏的根源是: 由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏

要避免内存泄漏有两种方式:

  1. 使用完 ThreadLocal,调用其 remove() 方法删除对应的 Entry
  2. 使用完 ThreadLocal,当前 Thread 也随之运行结束

第二种方式不好控制,特别是使用线程池的时候,线程结束后是不会被销毁的。

也就是说,只要在使用完 ThreadLocal 后及时调用 remove() 方法,可以有效避免内存泄漏

事实上,在 ThreadLocalMap 中的 set/get entry 方法中,如果 key 为 null ,也会将 value 设置为 null,而 key 是弱引用(指向当前线程实例)被回收时会置为 null,等下次调用 set/get entry 方法时对应的 value 也会被清除。

本章目录