ThreadLocal原理详解

ThreadLocal是线程本地变量,可以让每一个线程都拥有自己专属的本地变量,实现了线程的数据隔离。可以通过get()、set()来获取或更改值。

使用场景

  • 在spring事务中,保证一个线程下,一个事务的多个操作拿到的是一个Connection。
  • 在hiberate中管理session。
  • 在JDK8之前,为了解决SimpleDateFormat的线程安全问题。
  • 获取当前登录用户上下文。
  • 临时保存权限数据。
  • 使用MDC保存日志信息。

你工作中是否使用到ThreadLocal?

用的还真不多。项目中的工具类中有使用到,一个是DateUtils对时间进行格式化,另一个是NumberFormatUtil对数字的小数位进行保留。因为SimpleDateFormat和DecimalFormat都不是线程安全的。

NumberFormatUtil

public class NumberFormatUtil {
    private static final ThreadLocal<DecimalFormat> decimalFormatThreadLocal = new ThreadLocal<DecimalFormat>() {
        @Override
        protected DecimalFormat initialValue() {
            return new DecimalFormat("0.00");
        }
    };
    /**
     * 保留两位小数
     * @return
     */
    public static final String doubleToString(double value){
        return decimalFormatThreadLocal.get().format(value);
    }
}

DateUtils

public class DateUtils {
    private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {

        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyyMMdd");
        }
    };
    public static final Date convert(String source) throws ParseException {
        return df.get().parse(source);
    }
}

ThreadLoacl原理

ThreadLocal是线程的本地变量,实际上存储数据的并不是ThreadLocal,而是TreadLocalMap,每个线程(Thread类)中都有一个ThreadLocalMap(类似HashMap),key为ThreadLocal,value就是你set的值。(因为你一个线程可能会拥有多个ThreadLocal,所以要用一个Map来装着)

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

ThreadLocal 类的 set() 方法

public class ThreadLocal<T> {
     ...
     public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的成员变量ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //根据threadLocal对象从map中获取Entry对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                //获取保存的数据
                T result = (T)e.value;
                return result;
            }
        }
        //初始化数据
        return setInitialValue();
    }
    
    private T setInitialValue() {
        //获取要初始化的数据
        T value = initialValue();
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的成员变量ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        //如果map不为空
        if (map != null)
            //将初始值设置到map中,key是this,即threadLocal对象,value是初始值
            map.set(this, value);
        else
           //如果map为空,则需要创建新的map对象
            createMap(t, value);
        return value;
    }
    
    public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的成员变量ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        //如果map不为空
        if (map != null)
            //将值设置到map中,key是this,即threadLocal对象,value是传入的value值
            map.set(this, value);
        else
           //如果map为空,则需要创建新的map对象
            createMap(t, value);
    }
    
     static class ThreadLocalMap {
        ...
     }
     ...
}

ThreadLocalMap

static class ThreadLocalMap {
    /**
     * 键值对实体的存储结构
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        // 当前线程关联的 value,这个 value 并没有用弱引用追踪
        Object value;
        /**
         * 构造键值对
         *
         * @param k k 作 key,作为 key 的 ThreadLocal 会被包装为一个弱引用
         * @param v v 作 value
         */
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    // 初始容量,必须为 2 的幂
    private static final int INITIAL_CAPACITY = 16;

    // 存储 ThreadLocal 的键值对实体数组,长度必须为 2 的幂
    private Entry[] table;

    // ThreadLocalMap 元素数量
    private int size = 0;

    // 扩容的阈值,默认是数组大小的三分之二
    private int threshold;
}

ThreadLocalMap中包含了一个静态的内部类Entry,它继承了WeakReference。说明Entry是弱引用。

而Entry是否ThreadLocal作为key,value是你传进来的对象。

get()

public T get() {
    //1、获取当前线程
    Thread t = Thread.currentThread();
    //2、获取当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    //3、如果map数据不为空,
    if (map != null) {
        //3.1、获取threalLocalMap中存储的值
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为null
    return setInitialValue();
}


private T setInitialValue() {
    //initialValue是开放的一个模板方法,子类可以实现初始化生成一个value的方法,类似上面的阿里使用方式
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

getEntry()

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

描述了threadLoca的的hash冲突策略采用的是开放定址法,冲突+1。

ThreadLocal内存泄漏问题

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用(),而value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而value 不会被清理掉。这样⼀来, ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话, value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种 情况,在调用 set() 、 get() 、 remove()方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal 方法后 最好手动调用remove() ⽅法

弱引用
  • 如果⼀个对象只具有弱引用,那就类似于可有可无的生活用品。

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,⼀旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是⼀个优先级很低的线程, 因此不⼀定会很快发现那些只具有弱引用的对象。 弱引用可以和⼀个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收, Java虚拟机就会把这个弱引用加⼊到与之关联的引用队列中。

解决办法:
  1. 将ThreadLocal设置为空之前,执行remove()方法,会将key为空的键值对清空。
  2. 尽量将ThreadLocal设置成static。
  3. 非必要尽量不要在ThreadLocal中放大对象。

ThreadLocal、ThreadLocalMap、Thread三者之间的关系

image-20230921210108735

Thread类中已经包含了ThreadLocalMap,因此Thread在最外层。而ThreadLocalMap的key是ThreadLocal,因此ThreadLocalMap在第二层。

她们之间的引用关系:

image-20230921210835968

上图中ThreadLocal对象我画到了堆上,其实在实际的业务场景中不一定在堆上。因为如果ThreadLocal被定义成了static的,ThreadLocal的对象是类共用的,可能出现在方法区。

为什么用ThreadLocal做key?(为什么变量不是直接放到ThreadLocal上)

  • 如果你的项目中,只有一个ThreadLocal的话,直接通过ThreadLocal保存/用Thread作为key当然是可以的。
  • 但是如果我们项目保存多个ThreadLocal怎么办呢?
@Service
public class ThreadLocalService {
    private static final ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();
    private static final ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
    private static final ThreadLocal<Integer> threadLocal3 = new ThreadLocal<>();
}

因此,不能使用Thread做key,而应该改成用ThreadLocal对象做key,这样才能通过具体ThreadLocal对象的get方法。

为什么Entry的key为什么设计成弱引用?

  • 目的:尽最大可能避免内存泄漏!

引用关系:

image-20230921210835968
  1. 当ThreadLocal变量使用完后,对ThreadLocal对象的引用会被置为null。
  2. 而key对ThreadLocal对象是弱引用;即:随时可被回收。因此,key也会被置为null。
image-20230921214246153

关键点来了,ThreadLocal为了尽最大可能避免内存泄漏,是做了一些特殊处理的!

  • 此时,ThreadLocal变量为null。如果现在存在另外一个ThreadLocal变量b,并且调用了get、set或remove,三个方法中的任何一个方法,都会自动触发清理机制,将key为null的value值清空。(ThreadLocal可以通过key.get()==null来判断Key是否已经被回收,如果Key被回收,就说明当前Entry是一个废弃的过期节点,ThreadLocal会自发的将其清理掉。)因此,此时的Entry(ThreadLocalMap)就会被回收掉。

需要特别注意的地方是:

  1. key为null的条件是,ThreadLocal变量指向null,并且key是弱引用。如果ThreadLocal变量没有断开对ThreadLocal的强引用,即ThreadLocal变量没有指向null,GC就贸然的把弱引用的key回收了,不就会影响正常用户的使用?(重点)
  2. 如果当前ThreadLocal变量指向null了,并且key也为null了,但如果没有其他ThreadLocal变量触发get、set或remove方法,也会造成内存泄露。

那么,为什么不是value设计成弱引用呢?

  • 你value设置成弱引用了,那不就随时能被回收了吗?
  • key设置成弱引用不被回收,那是因为它弱引用了一个被强引用的对象。

如何实现父子线程共享数据?(线程池如何共享数据?)

  • 难道你们忘了吗?Thread类中还有另外一个变量:InheritableThreadLocal。
  • 使用方法和ThreadLocal类似,这里就不介绍拉!
ThreadLocal用完后一定要清理吗?你觉得为什么一定要remove?(阿里一面)
  • 答案是不一定要清理的。例如:SimpleDateFormat。
  • 因为我们往ThreadLocal中存放的是new SimpleDateFormat(),并不是存放具体的值。那么,不清理然后tomcat中的线程得到了这个复用,也不会产生问题。
网上很多人建议使用static修饰ThreadLocal,为什么?
  • ThreadLocal实现线程的数据隔离,并不在于自己本身,而是ThreadLocalMap。所以ThreadLocal可以只初始化一次。

部分引用:苏三说技术博主的文章

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值