十、ThreadLocal

一、ThreadLocal简介

1、概述

1、JDK1.2提供,位于java.lang包,ThreadLocal可以提供线程内的局部变量,这种变量在线程的生命周期内起作用,ThreadLocal又叫做线程本地变量或线程本地存储
2、实际上,就ThreadLocal这个类来讲,它不存储任何内容,真正存储数据的集合在每个Thread中的threadLocals变量里面,ThreadLocal中只是定义了这个集合的结构,并提供了一系列操作的方法。
3、作用:
  • 用于实现线程内的数据共享,某些数据是以线程为作用域并且不同线程具有不同的数据副本时,即数据在线程之间隔离(避免了线程安全问题),就可以考虑用ThreadLocal。即对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。
  • 方便同一个线程复杂逻辑下的数据传递,有些时候一个线程中的任务过于复杂,我们又需要某个数据能够贯穿整个线程的执行过程,可能涉及到不同类/函数之间数据的传递。此时使用Threadlocal存放数据,在线程内部只要通过get方法就可以获取到在该线程中存进去的数据,方便快捷。

2、常用方法

方法说明
public T get()返回当前线程的此线程局部变量副本中的值
protected T initialValue()返回此线程局部变量的当前线程的“初始值”
public void remove()删除此线程局部变量的当前线程值
public void set(T value)将此线程局部变量的当前线程副本设置为指定值
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier)创建一个线程局部变量。变量的初始值是通过调用Supplier上的get方法来确定的。
/**
 * @Date: 2022/8/14
 */
public class ThreadLocalTest {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("张三", "李四", "王五", "赵六", "陈七");
        User user = new User();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    int bonus = new Random().nextInt(5000) + 4000;
                    user.addSalary(bonus);
                    System.out.println(Thread.currentThread().getName() + " 的工资为:" + user.threadLocal.get());
                } finally {
                    // 如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄漏等问题
                    user.threadLocal.remove();
                }
            }, list.get(i)).start();
        }
    }
}

class User {
    // 工资
    int salary;

    // 初始化threadLocal
    ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 3000);

    public void addSalary(int bonus) {
        // 获取
        Integer basicSalary = threadLocal.get();
        salary = basicSalary + bonus;
        // 设置
        threadLocal.set(salary);
    }
}
/**
 * 运行结果如下:
 * 赵六 的工资为:8361
 * 张三 的工资为:9083
 * 王五 的工资为:9843
 * 李四 的工资为:7778
 * 陈七 的工资为:8191
 */

3、注意事项

1、在阿里Java开发手册中提到:必须回收自定义的ThreadLocal变量,尤其是在线程池场景下,线程经常会被复用,如果不清理自定义的ThradLocal变量,可能会影响后续业务逻辑和造成内存泄漏等问题。尽量在代码中使用try-finally块进行回收,在finally中调用remove()方法。代码演示及运行结果如下:

在这里插入图片描述

2、解决方法:使用try-finally块进行回收,在finally中调用remove()方法,正确结果如下:

在这里插入图片描述

/**
 * @Author: ye.yanbin
 * @Date: 2022/8/14
 */
public class ThreadLocalTest {
    public static void main(String[] args) {
        // 线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(3);

        User user = new User();
        try {
            // 没有回收threadLocal写法
            // for (int i = 0; i < 5; i++) {
            //     threadPool.submit(() -> {
            //         int bonus = new Random().nextInt(5000) + 4000;
            //         System.out.println(Thread.currentThread().getName() + " 初始值为:" + user.threadLocal.get());
            //         user.addSalary(bonus);
            //         System.out.println(Thread.currentThread().getName() + " 计算后的值为:" + user.threadLocal.get());
            //     }, threadPool);
            // }
            
            // 回收threadLocal写法
            for (int i = 0; i < 5; i++) {
                threadPool.submit(() -> {
                    try {
                        int bonus = new Random().nextInt(5000) + 4000;
                        System.out.println(Thread.currentThread().getName() + " 初始值为:" + user.threadLocal.get());
                        user.addSalary(bonus);
                        System.out.println(Thread.currentThread().getName() + " 计算后的值为:" + user.threadLocal.get());
                    } finally {
                        // 如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄漏等问题
                        user.threadLocal.remove();
                    }
                }, threadPool);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}

class User {
    // 工资
    int salary;

    // 初始化threadLocal
    ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 3000);

    public void addSalary(int bonus) {
        // 获取
        Integer basicSalary = threadLocal.get();
        salary = basicSalary + bonus;
        // 设置
        threadLocal.set(salary);
    }
}

二、ThreadLocal分析

1、Thread、ThreadLocal、ThreadLocalMap三者关系

1、每个Thread线程内部都定义有一个ThreadLocal.ThreadLocalMap类型的threadLocals变量,用于存放线程本地变量(key为ThreadLocal对象,value为要存储的数据),这样,线程之间的ThreadLocalMap互不干扰。threadLocals变量持有的ThreadLocalMap在ThreadLocal调用set或者get方法时才会初始化
public class Thread implements Runnable {
    // 与此线程相关的线程本地值,此ThreadLocalMap定义在ThreadLocal类中,使用在Thread类中
    ThreadLocal.ThreadLocalMap threadLocals = null;
}
2、ThreadLocal类中定义了一个内部类ThreadLocalMap,ThreadLocalMap是真正存放数据的容器,实际上它的底层就是一张哈希表。
3、ThreadLocal还提供相关方法,负责向当前线程的ThreadLocalMap变量获取和设置线程的变量值,相当于一个工具类。
public class ThreadLocal<T> {
    static class ThreadLocalMap {
        //……
    }

    /**
     * ThreadLocal的构造器,里面什么都没有,创建ThreadLocal时,没有初始化ThreadLocalMap
     */
    public ThreadLocal() {
    }
}
4、当在某个线程的方法中使用ThreadLocal设置值的时候,就会将该ThreadLocal对象添加到该线程内部的ThreadLocalMap中,其中键就是该ThreadLocal对象,值可以是任意类型任意值。当在某个线程的方法中使用ThreadLocal获取值的时候,会以该ThreadLocal对象为键,在该线程的ThreadLocalMap中获取对应的值
5、三者关系图:

在这里插入图片描述

2、ThreadLocalMap源码

1、ThreadLocalMap也是一张key-value类型的哈希表,但是ThreadLocalMap并没有实现Map接口,它内部具有一个Entry类型的table数组用于存放节点。Entry节点用于存放key、value数据,并且继承了WeakReference。
2、在创建ThreadLocalMap对象的同时即初始化16个长度的内部table数组,扩容阈值为len * 2 / 3,扩容为原容量的2倍,在没有使用ThreadLocal设置、获取值时,线程中的ThreadLocalMap对象一直为null。
static class ThreadLocalMap {
    /**
     * 内部节点对象,存放k的属性位于其父类WeakReference的父类Reference中,名为referent,即属性复用
     * 插入数据时,通过对key(threadLocal对象)的hash计算,来找出Entry应该存放的table数组的桶位,
     * 不过可能造成hash冲突,它采用线性探测法解决冲突,因此需要线性向后查找。
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        // 存放值
        Object value;
        // 构造器
        Entry(ThreadLocal<?> k, Object v) {
            // 调用父类的构造器,传入key,这里k被包装成为弱引用
            super(k);
            // 存放k的属性位于其父类WeakReference的父类Reference中,名为referent,即属性复用
            value = v;
        }
    }

    /**
     * table数组的初始化容量
     */
    private static final int INITIAL_CAPACITY = 16;

    /**
     * 存放数据的数组,在创建ThreadLocalMap对象时将会初始化该数组,大小必须是2^N次方
     */
    private Entry[] table;

    /**
     * table数组中元素个数
     */
    private int size = 0;

    /**
     * 扩容阈值,为len * 2 / 3
     */
    private int threshold; // Default to 0

    /**
     * 设置扩容大小阈值,为len * 2 / 3
     */
    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }
}

3、ThreadLocal的set方法

1、set方法是由ThreadLocal提供的,用于存放数据,大概步骤如下:
  • 获取当前线程的成员变量threadLocals
  • 如果threadLocals不等于null,则调用set方法存放数据,方法结束
  • 否则,调用createMap方法初始化threadLocals,然后存放数据,方法结束。
/*
 * 将当前线程局部变量的当前线程副本设置为指定值
 */
public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 通过getMap方法,获取当前线程t的threadLocals
    ThreadLocalMap map = getMap(t);
    // 如果threadLocals存在就存放数据,否则调用createMap方法初始化threadLocals
    if (map != null)
        // this代指当前ThreadLocal对象,value表示值
        map.set(this, value);
    else
        createMap(t, value);
}

/*
 * 获取指定线程t的threadLocals属性,该属性就是一个ThreadLocalMap,默认为null
 */
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

/*
 * 用于threadLocals初始化,创建一个ThreadLocalMap对象并赋值
 */
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

/*
 * 位于ThreadLocalMap中的构造器,用于创建新的ThreadLocalMap对象
 */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 初始化table数组,容量为16
    table = new Entry[INITIAL_CAPACITY];
    // 寻找数组桶位,通过ThreadLocal对象的threadLocalHashCode属性 & 15
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    // 该位置存放元素,由于是刚创建对象,因此不存在哈希冲突的情况,直接存储就行了
    table[i] = new Entry(firstKey, firstValue);
    // size设置为1
    size = 1;
    // 调用setThreshold方法,设置扩容阀值
    setThreshold(INITIAL_CAPACITY);
}
如果threadLocals不等于null,则调用ThreadLocalMap中的set方法存放数据
/*
 * 位于ThreadLocalMap内的set方法,用于存放数据
 */
private void set(ThreadLocal<?> key, Object value) {
	// tab保存数组引用
    Entry[] tab = table;
    // 获取数组长度
    int len = tab.length;
    // 哈希算法计算桶位,通过ThreadLocal的threadLocalHashCode属性计算出该key(ThreadLocal对象)对应的数组桶位i(即存储位置的下标索引)
    int i = key.threadLocalHashCode & (len-1);

    // 获取i索引位置的Entry e,如果e不为null,说明发生了哈希冲突,使用线性探测法替换或者存放数据
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        // 获取该Entry的key,即原来的ThreadLocal对象,这是其父类Reference的方法
        ThreadLocal<?> k = e.get();
		// 如果获取的ThreadLocal和要存的ThreadLocal是同一个对象,那么就替换值,方法结束
        if (k == key) {
            e.value = value;
            return;
        }
		/*
		 * 如果获取的ThreadLocal为null,这说明该WeakReference(弱引用)被回收了(因为Entry继承了WeakReference)
		 * 说明ThreadLocal肯定在外部没有强引用了,这个Entry变成了垃圾,擦除该位置的Entry,重新赋值并结束方法,这是为了防止内存泄漏
		 */
        if (k == null) {
            /*
             * 从该位置开始,继续寻找key,并且会尽可能清理其他无效slot位
             * 在replaceStaleEntry过程中,如果找到了key,则做一个swap把它放到那个无效slot中,value置为新值
             * 如果没有找到key,直接在该无效slot位原地放entry
             */
            replaceStaleEntry(key, value, i);
            return;
        }
    }
	/*
     * 执行到这一步方法还没有返回,说明i位置没有节点,此时e等于null,直接在该位置插入新的Entry
     * 此时肯定保证最初始的i和现在的之间的位置是存在节点的!
     */
    tab[i] = new Entry(key, value);
    // size自增1
    int sz = ++size;
    /*
     * 尝试清理垃圾,然后判断是否需要扩容,如果需要那就扩容
     * 存放完毕元素之后,再调用cleanSomeSlots做一次垃圾清理,如果没清理出去key(返回false)
     * 并且当前table大小大于等于阈值,则调用rehash方法
     * rehash方法中会调用一次全量清理slot方法也即expungeStaleEntries()方法
     * 如果expungeStaleEntries完毕之后table大小还是大于等于(threshold – threshold / 4),则调用resize方法进行扩容
     * resize方法将扩容两倍,同时完成节点的转移
     */
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        // 扩容
        rehash();
}

// 扩容
private void rehash() {
    expungeStaleEntries();
    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        resize();
}

4、ThreadLocal的get方法

1、对于不同的线程,每次获取变量值时,是从本线程内部的threadLocals中获取的,其他线程并不能获取到当前线程的值,形成了变量的隔离,互不干扰。大概步骤如下:
  • 获取当前线程的成员变量threadLocals
  • 如果threadLocals非空,调用getEntry方法尝试查找并返回节点e
  • 如果e不为null,说明找到了,那么返回e的value,方法结束
  • 如果e为null,说明没找到,方法继续。
/*
 * 返回当前线程局部变量的当前线程副本中的值
 */
public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 通过getMap方法,获取当前线程t的threadLocals
    ThreadLocalMap map = getMap(t);
    // 如果threadLocals不为null,说明已经初始化过
    if (map != null) {
        // 从threadLocals获取对应的Entry节点,传入this代表当前的ThreadLocal对象
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 如果Entry节点不为null
        if (e != null) {
            @SuppressWarnings("unchecked")
            // 获取Entry节点的值并返回
            T result = (T)e.value;
            return result;
        }
    }
    // 否则,如果threadLocals为null,或者Entry节点为null,那么返回null或者自定义的初始值
    return setInitialValue();
}

/*
 * 根据key,获取对应的Entry节点
 */
private Entry getEntry(ThreadLocal<?> key) {
    // 根据key计算桶位(Entry在数组中的索引位置)
    int i = key.threadLocalHashCode & (table.length - 1);
    // 根据索引获取Entry节点e
    Entry e = table[i];
    // 如果e不为null,并且e内部key等于当前key(ThreadLocal对象),则返回
    if (e != null && e.get() == key)
        return e;
    else
        // 否则使用线性探测查找,线性探测查找过程中每碰到无效slot,调用expungeStaleEntry进行清理;
        // 如果找到了则返回entry;没有找到,返回null
        return getEntryAfterMiss(key, i, e);
}

三、ThreadLocal的内存泄漏

1、概述

1、JVM垃圾回收的一些概念篇中提到过内存泄漏以及JVM可视化监控工具篇中也有对内存泄漏的补充说明
2、JVM垃圾回收的一些概念篇中提到过强、弱、软、虚四种对象引用

在这里插入图片描述

2、为什么使用弱引用包装的ThreadLocal对象作为key

1、如果某个Entry直接使用一个普通属性和ThreadLocal对象关联,即key是强引用。那么当最外面ThreadLocal对象的全局变量引用置空时,由于在ThreadLocalMap中存在key对这个ThreadLocal对象的强引用,那么这个ThreadLocal对象并不会被回收,但此时已经无法访问这个对象,就造成了key的内存泄漏
2、因此ThreadLocal对象被包装为弱引用作为key。当外部的ThreadLocal对象的强引用被清除时,由于在ThreadLocalMap中存储的是弱引用key,这个ThreadLocal对象只被弱引用对相关联,因此它就是一个弱引用对象,那么下一次GC时这个弱引用ThreadLocal对象可以自动被清除了
3、引发问题:
  • 由于Entry中的key(ThreadLocal对象)是弱引用,当外部的ThreadLocal对象的强引用被置为null时,那么系统GC时,根据可达性分析,这个ThreadLocal对象没有任何一条链路能够引用它,势必会被回收。
  • 这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如使用线程池,线程池中的线程会被复用),这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成value的内存泄漏。
  • 虽然弱引用,保证了key指向的ThreadLocal对象能被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现key为null时才会去回收整个entry、value,因此弱引用不能100%保证内存不泄漏。所以在不使用某个ThreadLocal对象后,要手动调用remove方法来删除它,尤其是在线程池中,不仅仅是内存泄漏的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug

在这里插入图片描述

4、从前面的set、getEntry、remove方法看出,在threadLocal的生命周期里,针对threadLocal存在的内存泄漏的问题,都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法清理掉key为null的脏entry

3、总结

1、ThreadLocal能实现线程的数据隔离,在于Thread的ThreadLocalMap,所以ThreadLocal可以只初始化一次,只分配一块内存空间即可,没必要作为成员变量多次被初始化,因此建议使用static修饰
2、ThreadLocalMap的key为ThreadLocal包装成的弱引用,value为设置的值。ThreadLocal会有内存泄漏的风险,因此使用完毕必须手动调用remove清除
3、应用场景:
  • 使用ThreadLocal的典型场景是数据库连接管理,线程会话管理等场景,只适用于独立变量副本的情况,如果变量为全局共享的,则不适用在高并发下使用。
  • Spring MVC对于每一个请求线程的Request对象使用ThreadLocal属性封装到RequestContextHolder中,这样每条线程都能访问到自己的Request。
  • Spring声明式事务管理中,每一个事务的信息也是使用ThreadLocal属性封装到TransactionSynchronizationManager中的,以此实现不同线程的事务存储和隔离,以及事务的挂起和恢复。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值