ThreadLocal | 基础知识

2022-06-20  本文已影响0人  七喜丶

一、底层结构

ThreadLocal底层有一个默认容量为16的数组组成,k是ThreadLocal对象的引用,v是要放到TheadLocal的值

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

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

数组类似为 HashMap,对哈希冲突的处理不是用链表/红黑树处理,而是使用链地址法,即尝试顺序放到哈希冲突下标的下一个下标位置

该数组也可以进行扩容

二、工作原理

一个 ThreadLocal 对象维护一个 ThreadLocalMap 内部类对象,ThreadLocalMap 对象才是存储键值的地方

更准确的说,是 ThreadLocalMap 的 Entry 内部类是存储键值的地方

见源码 set(),createMap() 可知

因为一个 Thread 对象维护了一个 ThreadLocal.ThreadLocalMap 成员变量,且 ThreadLocal 设置值时,获取的 ThreadLocalMap 正是当前线程对象的 ThreadLocalMap

// 获取 ThreadLocalMap 源码
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

所以每个线程对 ThreadLocal 的操作互不干扰,即 ThreadLocal 能实现线程隔离

三、使用

ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("Java");
String i = threadLocal.get()
// i = Java

四、为什么 ThreadLocal.ThreadLocalMap 底层是长度 16 的数组?

对ThreadLocal的操作见第三点,可以看到ThreadLocal每次set方法都是对同个key(因为是同个ThreadLocal对象,所以key肯定都是一样的)进行操作

如此操作,看似对ThreadLocal的操作永远只会存1个值,那用长度为1的数组它不香吗?为什么还要用16长度呢?

好了,其实这里有个需要注意的地方,ThreadLocal是可以存多个值的,代码如下:

ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("Java");
ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
threadLocal2.set("Java2");

按代码执行后,看着是new了2个ThreadLocal对象,但实际上,数据的存储都是在同一个ThreadLocal.ThreadLocalMap 上操作的

再次强调:ThreadLocal.ThreadLocalMap 才是数据存取的地方,ThreadLocal只是api调用入口,真相在ThreadLocal类源码的getMap()

因此上述代码最终结果就是一个ThreadLocalMap存了2个不同ThreadLocal对象作为key,对应value为Java,Java2

我们在看下ThreadLocal的set方法

public void set(T value) {
    Thread t = Thread.currentThread();
    // 这里每次 set 之前,都会调用 getMap(t) 方法,t 是当前调用 set 方法的线程
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

// 重点:返回调用 set 方法的线程(例子是主线程)的 ThreadLocal 对象。  
// 所以不管 api 调用方 new 多少个 ThreadLocal 对象,它永远都是返回调用线程(例子是主线程)的 ThreadLocal.ThreadLocalMap 对象供调用线程去存取数据。
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// t.threadLocals 的声明如下
ThreadLocal.ThreadLocalMap threadLocals = null;

// 仅有一个构造方法
public ThreadLocal() {
}

五、数据存放在数组中,如何解决hash冲突问题

使用链地址法解决

具体怎么解决?看看执行get、set方法的时候:

// 数组元素结构
Entry(ThreadLocal<?> k, Object v) {
    super(k);
    value = v;
}

六、ThreadLocal的内存泄露隐患

三个前置知识:

因为ThreadLocal作为弱引用被Entry中的key变脸引用,所以如果ThreadLocal没有外部强引用来引用它,那么ThreadLocal会在下次JVM垃圾收集时被回收

这个时候Entry中的key已经被回收,但value因为是强引用,所以不会被垃圾收集器回收。这样ThreadLocal的线程如果一直持续运行,value就一直得不到回收,导致发生内存泄露

如果想要避免内存泄漏,可以使用ThreadLocal对象的remove()方法

七、为什么ThreadLocalMap的key是弱引用

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

为什么要这样设计,这样分为两种情况来讨论:

八、父子线程如何共享ThreadLocal数据

  1. 主线程创建InheritableThreadLocal对象时,会为t.inheritableThreadLocals变量创建ThreadLocalMap,使其初始化。其中t是当前线程,即主线程
  2. 创建子线程时,在Thread的构造方法,会检查其父线程的inheritableThreadLocals是否为null。从第1步可知不为null,接着将父线程的inheritableThreadLocals变量值复制给这个子线程
  3. inheritableThreadLocals重写了getMap, createMap,使用的都是Thread.inheritableThreadLocals 变量

如下:


第 1 步:对 InheritableThreadLocal 初始化
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

第 2 步:创建子线程时,判断父线程的 inheritableThreadLocals 是否为空。非空进行复制
// Thread 构造方法中,一定会执行下面逻辑
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

第 3 步:使用对象为第 1 步创建的 inheritableThreadLocals 对象
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
}

// 示例:
// 结果:能够输出「父线程-Java」
ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("父线程-Java");
Thread t = new Thread(() -> System.out.println(threadLocal.get()));
t.start();

// 结果:null,不能够输出「子线程-Java」
ThreadLocal threadLocal2 = new InheritableThreadLocal();
Thread t2 = new Thread(() -> {
    threadLocal2.set("子线程-Java");
});
t2.start();
System.out.println(threadLocal2.get());
上一篇 下一篇

猜你喜欢

热点阅读