Java并发编程 —— ThreadLocal详解

一、什么是ThreadLocal

ThreadLocal用于提供线程内部共享的变量,每个线程在访问ThreadLocal实例的时候都可以获得自己的、独立初始化的变量副本,这样线程间互不干扰,从而避免了线程安全问题。

比如我们知道SimpleDateFormat是线程不安全的,多个线程同时用一个SimpleDateFormat对象解析日期时间会报错,但是在一个线程中每解析一个数据就创建一个SimpleDateFormat对象显然也很浪费,这时候我们就可以通过ThreadLocal.withInitial()方法创建一个ThreadLocal实例,并设定变量初始化函数,那么每个线程在第一次调用get()方法时就会执行这个初始化创建自己的SimpleDateFormat对象并返回,之后在线程中每次调用get()都是返回这个自己的独立的对象,从而实现了线程内的变量共享,并且线程间互不干扰。

public class DateFormatTest {
    public static void main(String[] args) {
        for (int i=1;i<=3;i++) {
            new Thread(() -> {
                try {
                    System.out.println(CommonUtils.parseTime("12:23:11"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
class CommonUtils {
    public static ThreadLocal<SimpleDateFormat> safeSdf =
            ThreadLocal.withInitial(() -> new SimpleDateFormat("hh:mm:ss"));
    public static long parseTime(String timeStr) throws ParseException {
        return safeSdf.get().parse(timeStr).getTime();
    }
}

除了withInitial()方法外,还可以直接调用ThreadLocal默认构造器创建实例,然后在需要设置线程共享变量时调用set()方法直接设置。

比如在请求Java后端的API服务时,http请求中经常会携带一些通用参数,比如token、uid、did这些,我们可以在请求预处理时将这些数据解析出来通过set()方法放到ThreadLocal中,然后在这个请求处理过程中此线程任意位置都可以直接访问到这些数据了,最后在请求结束后调用remove将数据移除就可以了。

public class TokenTL {
    public static ThreadLocal<String> tokenTL= new ThreadLocal<>();
    public static void setToken(String token) {tokenTL.set(token);}
    public static String setToken() {return tokenTL.get();}
    public static void removeToken() {tokenTL.remove();}
}

二、ThreadLocal的基本原理

线程Thread类中有一个ThreadLocalMap成员变量,一个线程所有通过ThreadLocal创建的独立变量实际上就是存放在这里面。

除了threadLocals外Thread对象中还有一个inheritableThreadLocals,在创建线程初始化时,会将父线程的inheritableThreadLocals中的键值引用拷贝一份到此线程的inheritableThreadLocals中,因此可通过这种方式实现父线程到子线程的数据通信。注意拷贝的是引用,如果在修改父线程中引用所指向对象中的数据,子线程也会受影响,但如果父线程直接调用threadLocal.set()方法替换map中的数据,子线程就无法感知了。

public class Thread implements Runnable {
    //......
    //与此线程有关的ThreadLocal值。由ThreadLocal类维护
    ThreadLocal.ThreadLocalMap threadLocals = null;

    //从父线程继承的InheritableThreadLocal值。由InheritableThreadLocal类维护
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    //......
}

ThreadLocalMap本身是ThreadLocal的一个内部类,可以把它理解为ThreadLocal 类实现的定制化的 HashMap。内部也是通过一个Entry数组存储键值对,键中存储对ThreadLocal对象的弱引用,值就是存储的独立变量。

1. set()方法

调用threadLocal.set()方法时,首先获取当前线程中的ThreadLocalMap对象,如果为null则创建,然后以当前threadLocal弱引用为key,以要set的值为value,在这个map中插入或者替换值。

public void set(T value) {
    //获取当前请求的线程
    Thread t = Thread.currentThread();
    //取出 Thread 类内部的 threadLocals 变量(哈希表结构)
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 将需要存储的值放入到这个哈希表中
        map.set(this, value);
    else
        createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

2. get()

在调用threadLocal.get()方法时,就是在当前线程的ThreadLocalMap中以threadLocal实例引用为key查找对应的value,如果没找到,则看threadLocal实例有没有重写InitialValue()函数,就是前面说的通过ThreadLocal.withInitial()方法创建实例时重写的,如果有则初始化并设置value值,然后将其返回,否则InitialValue()默认返回null

    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();
    }

3. remove()方法

调用threadLocal.set()方法时,清除当前线程的ThreadLocalMap中以当前ThreadLocal实例为key的Entry对象

三、ThreadLocal内存泄漏问题

线程中ThreadLocalMap对象的引用链如下:

Thread -> ThreadLocal.ThreadLocalMap -> Entry[] -> Enrty -> key(threadLocal对象)和value

其中key 为 ThreadLocal 对象的弱引用,而 value 是强引用。所以,如果这个 ThreadLocal 对象没有被外部强引用的情况下,在垃圾回收的时候,key 会被自动清理掉,而 value 不会被清理掉。

虽然在调用 set()、get()方法时清理部分 key 为 null 的记录,但这显然是不完备的,最好还是在使用完 ThreadLocal方法后手动调用remove()方法进行清除。

不过可能通常在使用ThreadLocal时都是直接定义为类变量(static修饰),默认被类强引用

1. 为什么 ThreadLocalMap 的 key 设计为弱引用?

在这里插入图片描述

2. 为什么 ThreadLocalMap 的 value 设计为强引用?

【假设Entry 的 value 是弱引用】:假设 key 所引用的 ThreadLocal 对象还被其他的引用对象强引用着,那么这个 ThreadLocal 对象就不会被 GC 回收,但如果 value 是弱引用且不被其他引用对象引用着,那 GC 的时候就被回收掉了,那线程通过 ThreadLocal 来获取 value 的时候就会获得 null,显然这不是我们希望的结果。因为对我们来说,value 才是我们想要保存的数据,ThreadLcoal 只是用来关联 value 的,如果 value 都没了,还要 ThreadLocal 干嘛呢?所以 value 不能是弱引用。

参考:https://zhuanlan.zhihu.com/p/513517989

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值