前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ThreadLocal 类精讲

ThreadLocal 类精讲

作者头像
Fisherman渔夫
发布2020-04-23 15:27:45
9070
发布2020-04-23 15:27:45
举报
文章被收录于专栏:渔夫渔夫

1. ThreadLocal 概述

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

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

2. 提供线程上下文能力

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

代码语言:javascript
复制
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() 方法的源码如下:

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

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

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

代码语言:javascript
复制
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<String, Long>

contextThreadLocal

threadContext

null

null

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

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

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

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

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

线程向一个 ThreadLocal 实例取数据的内部过程如下图所示:

ThreadLocal
ThreadLocal

线程对象借助于 ThreadLocal 来存储特定上下文数据,但是上下文数据位于线程而不位于 ThreadLocal 上,因此 ThreadLocal 进行上下文数据存储是线程安全的,这部分数据实际上是单线程独占的。

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

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

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

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

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

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

代码语言:javascript
复制
    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 的源码如下:

代码语言:javascript
复制
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(),方法声明如下:

代码语言:javascript
复制
    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 的:

代码语言:javascript
复制
    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);

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

代码语言:javascript
复制
        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<String, Long>

contextThreadLocal

threadContext

null

null

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

Key

Value

threadLocal1

childValue(“hello world”)

threadLocal2

childValue(HashMap<String, Long>)

contextThreadLocal

childValue(threadContext)

null

null

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

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. ThreadLocal 概述
  • 2. 提供线程上下文能力
  • 3. 线程上下文的延迟加载
  • 4. 单线程绑定多 ThreadLocal 实例
  • 5. 单 ThreadLocal 实例被多个线程共享
  • 6. 线程上下文的内存回收问题
  • 7. InheritableThreadLocal 类
相关产品与服务
数据保险箱
数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档