🌈写 SpringBoot 项目的时候,经常用到的一个保存用户信息的类就是 Threadlocal,我们今天就来详细介绍一下这个类。

# Threadlocal 有什么用:

📌简单的说就是,一个 ThreadLocal 在一个线程中是共享的,在不同线程之间又是隔离的(每个线程都只能看到自己线程的值)。如下图:

image.png

# ThreadLocal 使用实例

# API 介绍

🦄在使用 Threadlocal 之前我们先看以下它的 API:
16314961931.png

ThreadLocal 类的 API 非常的简单,在这里比较重要的就是 get ()、set ()、remove (),set 用于赋值操作,get 用于获取变量的值,remove 就是删除当前变量的值。需要注意的是 initialValue 方法会在第一次调用时被触发,用于初始化当前变量值,默认情况下 initialValue 返回的是 null。

# ThreadLocal 的使用

🍋说完了 ThreadLocal 类的 API 了,那我们就来动手实践一下了,来理解前面的那句话:一个 ThreadLocal 在一个线程中是共享的,在不同线程之间又是隔离的(每个线程都只能看到自己线程的值)

public class ThreadLocalTest {
    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
	// 重写这个方法,可以修改 “线程变量” 的初始值,默认是 null
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };
    public static void main(String[] args) throws InterruptedException {
        // 一号线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("一号线程set前:" + threadLocal.get());
                threadLocal.set(1);
                System.out.println("一号线程set后:" + threadLocal.get());
            }
        }).start();
        // 二号线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("二号线程set前:" + threadLocal.get());
                threadLocal.set(2);
                System.out.println("二号线程set后:" + threadLocal.get());
            }
        }).start();
        // 主线程睡 1s
        Thread.sleep(1000);
        // 主线程
        System.out.println("主线程的threadlocal值:" + threadLocal.get());
    }
}

稍微解释一下上面的代码:💡

每一个 ThreadLocal 实例就类似于一个变量名,不同的 ThreadLocal 实例就是不同的变量名,它们内部会存有一个值(暂时这么理解)在后面的描述中所说的 “ThreadLocal 变量或者是线程变量” 代表的就是 ThreadLocal 类的实例。

在类中创建了一个静态的 “ThreadLocal 变量”,在主线程中创建两个线程,在这两个线程中分别设置 ThreadLocal 变量为 1 和 2。然后等待一号和二号线程执行完毕后,在主线程中查看 ThreadLocal 变量的值。

程序结果及分析⌛
16314972101.png

程序结果重点看的是主线程输出的是 0,如果是一个普通变量,在一号线程和二号线程中将普通变量设置为 1 和 2,那么在一二号线程执行完毕后在打印这个变量,输出的值肯定是 1 或者 2(到底输出哪一个由操作系统的线程调度逻辑有关)。但使用 ThreadLocal 变量通过两个线程赋值后,在主线程程中输出的却是初始值 0。在这也就是为什么 “一个 ThreadLocal 在一个线程中是共享的,在不同线程之间又是隔离的”,每个线程都只能看到自己线程的值,这也就是 ThreadLocal 的核心作用:实现线程范围的局部变量。

# Threadlocal 的源码分析

# 原理

每个 Thread 对象都有一个 ThreadLocalMap,当创建一个 ThreadLocal 的时候,就会将该 ThreadLocal 对象添加到该 Map 中,其中键就是 ThreadLocal,值可以是任意类型。 这句话刚看可能不是很懂,下面我们一起看完源码就明白了。

前面我们的理解是所有的常量值或者是引用类型的引用都是保存在 ThreadLocal 实例中的,但实际上不是的,这种说法只是让我们更好的理解 ThreadLocal 变量这个概念。向 ThreadLocal 存入一个值,实际上是向当前线程对象中的 ThreadLocalMap 存入值,ThreadLocalMap 我们可以简单的理解成一个 Map,而向这个 Map 存值的 key 就是 ThreadLocal 实例本身。

# 源码

image.png

👉也就是说,想要存入的 ThreadLocal 中的数据实际上并没有存到 ThreadLocal 对象中去,而是以这个 ThreadLocal 实例作为 key 存到了当前线程中的一个 Map 中去了,获取 ThreadLocal 的值时同样也是这个道理。这也就是为什么 ThreadLocal 可以实现线程之间隔离的原因了。

# 内部类 ThreadLocalMap

ThreadLocalMap 是 ThreadLocal 的内部类,实现了一套自己的 Map 结构✨

ThreadLocalMap 属性:

static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        // 初始容量 16
        private static final int INITIAL_CAPACITY = 16;
        // 散列表
        private Entry[] table;
        //entry 有效数量 
        private int size = 0;
        // 负载因子
        private int threshold;

ThreadLocalMap 设置 ThreadLocal 变量

private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            
            // 与运算  & (len-1) 这就是为什么 要求数组 len 要求 2 的 n 次幂 
            // 因为 len 减一后最后一个 bit 是 1 与运算计算出来的数值下标 能保证全覆盖 
            // 否者数组有效位会减半 
            // 如果是 hashmap 计算完下标后 会增加链表 或红黑树的查找计算量 
            int i = key.threadLocalHashCode & (len-1);
            
            // 从下标位置开始向后循环搜索  不会死循环  有扩容因子 必定有空余槽点
            for (Entry e = tab[i];   e != null;  e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                // 一种情况 是当前引用 返回值
                if (k == key) {
                    e.value = value;
                    return;
                }
                // 槽点被 GC 掉 重设状态 
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
			// 槽点为空 设置 value
            tab[i] = new Entry(key, value);
            // 设置 ThreadLocal 数量
            int sz = ++size;
			
			// 没有可清理的槽点 并且数量大于负载因子 rehash
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

ThreadLocalMap 属性介绍😎:

  • 和普通 Hashmap 类似存储在一个数组内,但与 hashmap 使用的拉链法解决散列冲突不同的是 ThreadLocalMap 使用开放地址法
  • 数组 初始容量 16,负载因子 2/3
  • node 节点 的 key 封装了 WeakReference 用于回收

# ThreadLocalMap 存储位置

储存在 Thread 中,有两个 ThreadLocalMap 变量

16315167211.png

  1. threadLocals 在 ThreadLocal 对象方法 set 中去创建 也由 ThreadLocal 来维护
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
  1. inheritableThreadLocals 和 ThreadLocal 类似 InheritableThreadLocal 重写了 createMap 方法
void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
  1. inheritableThreadLocals 作用是将 ThreadLocalMap 传递给子线程

image.png

  1. init 方法中 条件满足后直接为子线程创建 ThreadLocalMap

image.png

注意:

  1. 仅在初始化子线程的时候会传递 中途改变副线程的 inheritableThreadLocals 变量 不会将影响结果传递到子线程 。
  2. 使用线程池要注意 线程不回收 尽量避免使用父线程的 inheritableThreadLocals 导致错误

# Key 的弱引用问题

为什么要用弱引用,官方是这样回答的

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
为了处理非常大和生命周期非常长的线程,哈希表使用弱引用作为 key。

生命周期长的线程可以理解为:线程池的核心线程

ThreadLocal 在没有外部对象强引用时如 Thread,发生 GC 时弱引用 Key 会被回收,而 Value 是强引用不会回收,如果创建 ThreadLocal 的线程一直持续运行如线程池中的线程,那么这个 Entry 对象中的 value 就有可能一直得不到回收,发生内存泄露。

  • key 使用强引用🌴: 引用的 ThreadLocal 的对象被回收了,但是 ThreadLocalMap 还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 不会被回收,导致 Entry 内存泄漏。
  • key 使用弱引用🌴: 引用的 ThreadLocal 的对象被回收了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 也会被回收。value 在下一次 ThreadLocalMap 调用 set,get,remove 的时候会被清除。

Java8 中已经做了一些优化如,在 ThreadLocal 的 get ()、set ()、remove () 方法调用的时候会清除掉线程 ThreadLocalMap 中所有 Entry 中 Key 为 null 的 Value,并将整个 Entry 设置为 null,利于下次内存回收。

# java 中的四种引用

  1. 强引用📍: 如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM 也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。如果想中断强引用和某个对象之间的关联,可以显式地将引用赋值为 null,这样一来的话,JVM 在合适的时间就会回收该对象
  2. 软引用📍: 在使用软引用时,如果内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器回收,只有在内存不足时,软引用才会被垃圾回收器回收。(软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性)
  3. 弱引用📍: 具有弱引用的对象拥有的生命周期更短暂。因为当 JVM 进行垃圾回收,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。不过由于垃圾回收器是一个优先级较低的线程,所以并不一定能迅速发现弱引用对象
  4. 虚引用📍: 虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。(注意哦,其它引用是被 JVM 回收后才被传入 ReferenceQueue 中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。可以使用在对象销毁前的一些操作,比如说资源释放等。)

通常 ThreadLocalMap 的生命周期跟 Thread(注意线程池中的 Thread)一样长,如果没有手动删除对应 key(线程使用结束归还给线程池了,其中的 KV 不再被使用但又不会 GC 回收,可认为是内存泄漏),一定会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用 ThreadLocal 会被 GC 回收,不会内存泄漏,对应的 value 在下一次 ThreadLocalMap 调用 set,get,remove 的时候会被清除,Java8 已经做了上面的代码优化。

# 总结:

ThreadLocal 的作用: 实现线程范围内的局部变量,即 ThreadLocal 在一个线程中是共享的,在不同线程之间是隔离的。

ThreadLocal 的原理: ThreadLocal 存入值时使用当前 ThreadLocal 实例作为 key(并不是以当前线程对象作为 key),存入当前线程对象中的 Map 中去。最开始在看源码之前,我以为是以当前线程对象作为 key 将对象存入到 ThreadLocal 中的 Map 中去....

🚀🚀🚀

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

田埂、 微信支付

微信支付

田埂、 支付宝

支付宝

田埂、 贝宝

贝宝