ThreadLocal 类

ThreadLocal 类

文章目录

  • ThreadLocal 类
    • 1. ThreadLocal 概述
    • 2. 提供线程上下文能力
    • 3. 线程上下文的延迟加载
    • 4. 单线程绑定多 ThreadLocal 实例
    • 5. 单 ThreadLocal 实例被多个线程共享
    • 6. 线程上下文的内存回收问题
    • 7. InheritableThreadLocal 类

1. ThreadLocal 概述

ThreadLocal 类在多线程中很常见,其主要的特性可以概括为以下五点:

  1. 具备向所有线程提供上下文的能力;
  2. 延迟创建线程上下文实例;
  3. 一个线程可以绑定多个 ThreadLocal 实例;
  4. 同一个 ThreadLocal 可以被多个线程同时绑定;
  5. ThreadLocal 是一个彻底的工具类,本身不存储任何上下文信息;

2. 提供线程上下文能力

Context,上下文,其通常指能够提供环境、临时存储数据的实例。ThreadLocal 是一种上下文实例,Thread 可以通过调用其 set() 以及 get() 方法轻松地存取数据实例,如下代码案例所示:

public class Test {
    public static void main(String[] args) {
        /**
         * 方式 1
         */
        final ThreadLocal<String> threadLocal1 = new ThreadLocal();
        threadLocal1.set("hello world");

        /**
         * 方式 2
         */
        final ThreadLocal<Map<String,Long>> threadLocal2 = new ThreadLocal();

        threadLocal2.set(new HashMap<String, Long>());

        threadLocal2.get().put("k1",100000000000001L);


        /**
         * 方式 3
         */

        final ThreadLocal<ThreadContext> contextThreadLocal = new ThreadLocal<>();

        contextThreadLocal.set(new ThreadContext());

        final ThreadContext threadContext = contextThreadLocal.get();
        
        //利用 threadContext 进行一些数据存取工作

    }
}

class ThreadContext{

}

这里的线程都是 main 线程。

  • 方式 1 说明,每一个线程实例在同一个 ThreadLocal 实例中仅仅能够放置一个实例,类型是任意的。
  • 方式 2 说明,通过放置的实例限制为 Map 类型,实际上我们能够存储很多数据;
  • 方式 3 则是很多框架中使用的方式,提供一个额外的线程上下文类型,然后每个线程都将自身作为 key,额外的线程上下文实例(比如实例代码的 ThreadContext 类型)作为 value。存取数据都是再从 ThreadLocal 中得到线程对应的 ThreadContext 实例后,对 ThreadContext 实例进行存储属于。

Thread 的线程上下文可以是任意类型,因为事实上,Thread 并没有规定上下文的类型。上下文可以仅仅是一个 String,也可以是一个特殊的 Context 类型。

3. 线程上下文的延迟加载

ThreadLocal.set() 方法的源码如下:

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
//得到当前 Thread 内部的 threadLocals 实例
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
  void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

set() 方法的执行逻辑是:判断当前 Thread 实例内部的 threadLocals 有没有初始化(没有则是 null),如果没有则调用 createMap() 方法给当前线程的 threadLocals 进行初始化。如果已经初始化了,那么就将本地的 this-value 键值对覆盖掉。

Thread 类内部的 threadLocals 变量定义如下:

ThreadLocal.ThreadLocalMap threadLocals = null;

从初始化方法 createMap() 方法可以知道,此 Map 的 key 为当前 ThreadLocal 实例,value 为 set 方法入口参数。

由源代码分析可知,如果一个线程不调用 ThreadLocal 实例的 set() 方法,那么 Thread 的 threadLocals 内部实例永远得不到初始化。这就是线程的上下文延迟加载,延迟,指的是相对于线程实例初始化延迟。

4. 单线程绑定多 ThreadLocal 实例

第二节实际上已经体现了单线程能够绑定多个 ThreadLocal 实例,我们为 main 线程绑定了 3 个 ThreadLocal 实例,并且 main 线程向它们存储的上下文数据是互不影响的。

ThreadLocal 实例是如何做到与多个 Map 绑定?

这是因为 Thread 类内部的 ThreadLocal.ThreadLocalMap 是一个定制版本的 HashMap,其 key 为 ThreadLocal,value 为线程为其放置的数据。

所以在第二节中的 main 线程中的 ThreadLocalMap 可以用下表表示:

Main 线程内部 ThreadLocal.ThreadLocalMap 实例的内部结构:

Key Value
threadLocal1 “hello world”
threadLocal2 HashMap
contextThreadLocal threadContext
null null

注意,当 ThreadLocalMap 初始化时的大小是 16。

所以当 Main 线程调用 threadLocal1.get() 方法时,实际上是再访问自己内部的 ThreadLocal.ThreadLocalMap 实例,访问的 key 为 threadLocal1,返回的结果是 “hello world”。

为什么会出现一个线程绑定多个 ThreadLocal 实例的情况?

这是因为 ThreadLocal 是一个泛型类,定义为:public class ThreadLocal {},如果线程想要存储多种不同类型的数据于上下文,那么最简单的方式就是给不同泛型类型 ThradLocal 存储数据。

5. 单 ThreadLocal 实例被多个线程共享

线程向一个 ThreadLocal 实例取数据的内部过程如下图所示:
ThreadLocal 类_第1张图片
线程对象借助于 ThreadLocal 来存储特定上下文数据,但是上下文数据位于线程而不位于 ThreadLocal 上,因此 ThreadLocal 进行上下文数据存储是线程安全的,这部分数据实际上是单线程独占的。

某个具体的 ThreadLocal限制了所有线程向其存取数据的类型只能为 T 类型,如果要存取其他类型,线程只好找另一个类型合适的 ThreadLocal 实例。

有上述图可见,ThreadLocal 是一个彻底的工具类,本身不存储任何上下文信息,不同 ThreadLocal 实例之间仅仅是泛型 T 不同,执行的逻辑是完全相同的。

6. 线程上下文的内存回收问题

一旦线程运行结束,与其配套的线程上下文也应当被垃圾回收。

虽然线程存储上下文数据时借助于多个 ThreadLocal,ThreadLocal 实例又可能被多个 Thread 共享,但是上下文数据始终存储于 Thread 实例中,所以上下文数据是否被回收取决于 Thread 实例,而不是 ThreadLocal 实例。

当线程运行结束后,JVM 会调用 Thread 的 exit() 方法:

    private void exit() {
        if (group != null) {
            group.threadTerminated(this);
            group = null;
        }
        /* Aggressively null out all reference fields: see bug 4006245 */
        target = null;
        /* Speed the release of some of these resources */
        threadLocals = null;
        inheritableThreadLocals = null;
        inheritedAccessControlContext = null;
        blocker = null;
        uncaughtExceptionHandler = null;
    }

这里将 threadLocals 变量赋值为 null,目的是方便垃圾回收器回收上下文数据所占据的内存空间。

7. InheritableThreadLocal 类

线程上下文可以利用 ThreadLocal 类实现。而线程中有一个概念:父线程和子线程。父线程负责创建子线程,并且我们希望父线程能够利用子线程的上下文,所以提供了 InheritableThreadLocal 类。

InheritableThreadLocal 的源码如下:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    protected T childValue(T parentValue) {
        return parentValue;
    }

        ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

其继承于 ThreadLocal() 实例,其仅仅重写了 ThreadLocal 类的 3 个方法。

  • childValue() 方法:此方法在 ThreadLocal 实例中调用直接会抛出异常。而在 InheritableThreadLocal 类中代表将父类的 value 值转换为子类的 value,默认实现是不进行转换。如果需要转换则应当重写此方法。
  • getMap() 方法:重写的目的在于返回线程的 inheritableThreadLocals 实例,原本是返回线程的 ThreadLocal 实例。
  • creteMap()方法:重写的目的在于原本方法会将类型为 ThreadLocal 的 this 作为键值,重写后将类型为 InheritableThreadLocal 的 this 作为键值。

后面两个方法和 ThreadLocal 的设计没有任何区别,InheritableThreadLocal 的特点在于其并非是延迟加载的。

当利用 new 关键字构造一个 Thread 实例时,总是会调用 init(),方法声明如下:

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {//...}

其中,参数 boolean inheritThreadLocals 如果为 true,那么就会使正在构造的线程的 inheritableThreadLocals 实例得到父线程的 inheritableThreadLocals,在默认情况下此值就是为 true。所以简单调用 new Thread(),构造的线程实例会拥有当前线程的上下文数据的引用。

下面看看 init() 方法是如何将父线程的 ThreadLocal 数据引用给予子线程的 InheritableThreadLocal 的:

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc) {
                // 省略无关代码
                ...
                Thread parent = currentThread();
                ...
                // 省略无关代码
                ...
         if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

然后其内部方法栈最终会调用如下的构造方法:

        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

此构造方法最终完成了将父线程的 inheritableThreadLocals 赋值给子线程 inheritableThreadLocals。注意在子线程的 inheritableThreadLocals 的 Key 仍然为父线程 inheritableThreadLocals 中的键值,但是 value 因为调用了 childValue() 方法可能会进行转变。

综上所述,InheritableThreadLocals 和 ThreadLocal 最大的不同在于前者有父子线程的继承性,且赋值过程不是延迟加载,而是构造时就加载。

整个过程如下面两个表所示:

父线程有如下所示的 InheritableThreadLocals 内部实例(注意其类型,不为 ThreadLocal)

Key Value
threadLocal1 “hello world”
threadLocal2 HashMap
contextThreadLocal threadContext
null null

且父线程在创建子线程时,inheritThreadLocals 参数为 true,那么此时子线程的内部实例 InheritableThreadLocals 数据如下表所示:

Key Value
threadLocal1 childValue(“hello world”)
threadLocal2 childValue(HashMap)
contextThreadLocal childValue(threadContext)
null null

所以父线程在设计其上下文时,如果打算将某些上下文数据对子线程可见(具有继承性可以继续传给下一个子线程),那么应当将这部分数据放到 inheritThreadLocals 实例中去。如果部分子线程觉得自己完全没有必要得到父线程的上下文,那么在构造时就将 init() 方法的入口参数 inheritThreadLocals 参数设置为 false。

父线程可以决定将哪些上下文用于分享给子线程,子线程在构造时通过修改入口参数,也有充分地自由度拒绝父线程的上下文信息。

你可能感兴趣的:(Java)