乐趣区

关于java:有关-ThreadLocal-的一切

早上好,各位新老读者们,我是七淅 (xī)。

明天和大家分享的是面试常驻嘉宾:ThreadLocal

当初鹅厂一面就有问到它,问题的答案在上面注释的第 2 点。

1. 底层构造

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,对哈希抵触的解决不是用链表 / 红黑树解决,而是应用链地址法,即尝试程序放到哈希抵触下标的下一个下标地位。

该数组也能够进行扩容。

2. 工作原理

一个 ThreadLocal 对象保护一个 ThreadLocalMap 外部类对象,ThreadLocalMap 对象才是存储键值的中央。

更精确的说,是 ThreadLocalMap 的 Entry 外部类是存储键值的中央

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

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

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

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

3. 应用

ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("七淅在学 Java");
Integer i = threadLocal.get()
// i = 七淅在学 Java

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

对 ThreadLocal 的操作见第 3 点,能够看到 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() {}

5. 数据寄存在数组中,那如何解决 hash 抵触问题

应用链地址法解决。

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

  • set:

    • 依据 ThreadLocal 对象的 hash 值,定位到 ThreadLocalMap 数组中的地位。
    • 如果地位无元素则间接放到该地位
    • 如果有元素

      • 且数组的 key 等于该 ThreadLocal,则笼罩该地位元素
      • 否则就找下一个空地位,直到找到空或者 key 相等为止。
  • get:

    • 依据 ThreadLocal 对象的 hash 值,定位到 ThreadLocalMap 数组中的地位。
    • 如果不统一,就判断下一个地位
    • 否则则间接取出
// 数组元素构造
Entry(ThreadLocal<?> k, Object v) {super(k);
    value = v;
}

6. ThreadLocal 的内存泄露隐患

三个前置常识:

  • ThreadLocal 对象保护一个 ThreadLocalMap 外部类
  • ThreadLocalMap 对象又保护一个 Entry 外部类,并且该类继承弱援用 WeakReference<ThreadLocal<?>>,用来寄存作为 key 的 ThreadLocal 对象(可见最下方的 Entry 构造方法源码),可见最初的源码局部。
  • 不论以后内存空间足够与否,GC 时 JVM 会回收弱援用的内存

因为 ThreadLocal 作为弱援用被 Entry 中的 Key 变量援用,所以如果 ThreadLocal 没有内部强援用来援用它,那么 ThreadLocal 会在下次 JVM 垃圾收集时被回收。

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

如果想要防止内存透露,能够应用 ThreadLocal 对象的 remove() 办法

7. 为什么 ThreadLocalMap 的 key 是弱援用

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

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

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

  • key 应用强援用:只有创立 ThreadLocal 的线程还在运行,那么 ThreadLocalMap 的键值就都会内存透露,因为 ThreadLocalMap 的生命周期同创立它的 Thread 对象。
  • key 应用弱援用:是一种解救措施,起码弱援用的值能够被及时 GC,加重内存透露。另外,即便没有手动删除,作为键的 ThreadLocal 也会被回收。因为 ThreadLocalMap 调用 set、get、remove 时,都会先判断之前该 value 对应的 key 是否和以后调用的 key 相等。如果不相等,阐明之前的 key 曾经被回收了,此时 value 也会被回收。因而 key 应用弱援用是最优的解决方案。

8.(父子线程)如何共享 ThreadLocal 数据

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

如下:

public class InheritableThreadLocal<T> extends ThreadLocal<T> 

要害源码:第 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());

文章首发公众号:七淅在学 Java,继续原创输入 Java 后端干货。

如果对你有帮忙的话,能够给个赞再走吗

退出移动版