• ThreadLocal的使用及原理解析



    # 基本使用

    JDK的lang包下提供了ThreadLocal类,我们可以使用它创建一个线程变量,线程变量的作用域仅在于此线程内。
    用2个示例来展示一下ThreadLocal的用法。

    **示例一:**
    ```java
    ThreadLocal threadLocal = new ThreadLocal<>();

    System.out.println(threadLocal.get());
    threadLocal.set(1);
    System.out.println(threadLocal.get());
    threadLocal.remove();
    System.out.println(threadLocal.get());
    ```
    输出:
    ```powershell
    null
    1
    null
    ```

    这个示例展示了ThreadLocal提供的所有方法,ThreadLocal中提供了三个方法,分别是:

    - get:获取变量值
    - set:设置变量值
    - remove:删除变量值

    **示例二:**
    ```java
    //    创建一个MyRun类
    class MyRun implements Runnable {

        //    创建2个线程变量,var1、var2
        private ThreadLocal var1 = new ThreadLocal<>();
        private ThreadLocal var2 = new ThreadLocal<>();

        @Override
        public void run() {
            //    循环调用m方法5次
            for (int i = 0; i < 5; i++) {
                m();
            }
        }

        public void m() {
            //    当前线程名称
            String name = Thread.currentThread().getName();

            //    var1变量从1开始,m每次调用递增1
            Integer v = var1.get();
            if(v == null) {
                var1.set(1);
            }else {
                var1.set(v + 1);
            }

            //    var2变量 = 线程名 - var1值
            var2.set(name + "-" + var1.get());

            //    打印
            print();
        }

        public void print() {
            String name = Thread.currentThread().getName();
            System.out.println(name + ", var1: " + var1.get() + ", var2: " + var2.get());
        }
    }
    ```

    创建2个线程,执行同一个MyRun:
    ```java
    MyRun myRun = new MyRun();
    Thread t1 = new Thread(myRun);
    Thread t2 = new Thread(myRun);
    t1.start();
    t2.start();
    ```
    输出:
    ```powershell
    Thread-0, var1: 1, var2: Thread-0-1
    Thread-1, var1: 1, var2: Thread-1-1
    Thread-0, var1: 2, var2: Thread-0-2
    Thread-1, var1: 2, var2: Thread-1-2
    Thread-0, var1: 3, var2: Thread-0-3
    Thread-1, var1: 3, var2: Thread-1-3
    Thread-0, var1: 4, var2: Thread-0-4
    Thread-0, var1: 5, var2: Thread-0-5
    Thread-1, var1: 4, var2: Thread-1-4
    Thread-1, var1: 5, var2: Thread-1-5
    ```

    示例二展示了ThreadLocal的重要特点:
    两个线程执行的是同一个MyRun对象,如果var1、var2是普通的成员变量,两个线程访问的将是同一个变量,这将会产生线程安全问题,然而从输出日志看来,t1、t2的var1、var2值其实是独立的,互不影响的。

    这是因为var1、var2是ThreadLocal类型,即是线程变量,它是绑定在线程上的,哪个线程来访问这段代码,就从哪个线程上获取var1、var2变量值,线程与线程之间是相互隔离的,因此也不存在线程安全问题。

    # 原理解析

    ThreadLocal是如何实现这个效果的呢?
    我们可以从ThreadLocal的源代码中一探究竟。

    其中,最关键是get方法,我将get相关的源代码都提取出来如下:
    ```java
    public T get() {
        //    获取当前线程对象
        Thread t = Thread.currentThread();
        //    从当前线程中获取ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        
        if (map != null) {
            //    从ThreadLocalMap对象中获取当前ThreadLocal对应Entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                //    若Entry不为null,返回值
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        
        //    如果获取ThreadLocalMap对象为null则返回默认值
        return setInitialValue();
    }

    //    从指定线程对象获取ThreadLocalMap,也就是Thread中的threadLocals
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    //    默认值
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        
        if (map != null)
            map.set(this, value);//      如果当前线程的threadLocals不为null,则赋默认值
        else
            createMap(t, value);  //    如果当前线程的threadLocals为null,则新建
        return value;
    }

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

    protected T initialValue() {
        return null;  //  初始值是null
    }

    ```

    从以上这段代码可以看出,ThreadLocal访问的实际上是当前线程的成员变量threadLocals。
    threadLocals的数据类型是ThreadLocalMap,这是JDK中专门为ThreadLocal设计的数据结构,它本质就是一个键值对类型。
    ThreadLocalMap的键存储的是当前ThreadLocal对象,值是ThreadLocal对象实际存储的值。
    当用ThreadLocal对象get方法时,它实际上是从当前线程的threadLocals获取键为当前ThreadLocal对象所对应的值。

    画张图来辅助一下理解:
    ![](https://cdn.nlark.com/yuque/0/2022/jpeg/2565452/1667566447745-4dac46dc-c1ad-4a10-b62b-5d9a8506c8fa.jpeg)

    清楚了ThreadLocal的get原理,set和remove方法不需要看源码也能猜出是怎么写的。
    无非是以ThreadLocal对象为键设置其值或删除键值对。

    # ThreadLocal的初始值

    上面的介绍,我们看到ThreadLocal的initialValue方法永远都是返回null的:
    ```java
    protected T initialValue() {
        return null;  //  初始值是null
    }
    ```

    如果想要设定ThreadLocal对象的初始值,可以用以下方法:
    ```java
    ThreadLocal threadLocal = ThreadLocal.withInitial(()->1);
    System.out.println(threadLocal.get());
    ```
    withInitial方法内实际返回的是一个ThreadLocal子类SuppliedThreadLocal对象。
    SuppliedThreadLocal重写了ThreadLocal的initialValue方法。
    ```java
    static final class SuppliedThreadLocal extends ThreadLocal {

        private final Supplier supplier;

        SuppliedThreadLocal(Supplier supplier) {
            this.supplier = Objects.requireNonNull(supplier);
        }

        @Override
        protected T initialValue() {
            return supplier.get();
        }
    }
    ```

    # 获取父线程的ThreadLocal变量

    在一些场景下,我们可能需要子线程能获取到父线程的ThreadLocal变量,但使用ThreadLocal是无法获取到的:
    ```java
    public static ThreadLocal threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        threadLocal.set(1);
        System.out.println(threadLocal.get());

        Thread childThread = new Thread(() -> System.out.println(threadLocal.get()));
        childThread.start();
    }
    ```
    输出:
    ```powershell
    1
    null
    ```

    使用ThreadLocal的子类**InheritableThreadLocal**可以达到这个效果:
    ```java
    public static ThreadLocal threadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {
        threadLocal.set(1);
        System.out.println(threadLocal.get());

        Thread childThread = new Thread(() -> System.out.println(threadLocal.get()));
        childThread.start();
    }
    ```
    ```powershell
    1
    1
    ```

    **InheritableThreadLocal是怎么做到的呢?**

    我们来分析一下InheritableThreadLocal的源代码。
    ```java
    public class InheritableThreadLocal extends ThreadLocal {
        
        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);
        }
    }
    ```
    InheritableThreadLocal的源代码并不多,主要是**覆盖了ThreadLocal的三个方法childValue、getMap、createMap。**
    childValue方法用于ThreadLocalMap内部使用,我们不打算讲解ThreadLocalMap内部设计,这里可以忽略;
    ThreadLocal本来getMap、createMap读写的是当前Thread对象的threadLocals变量。
    而InheritableThreadLocal将其改为了读写当前Thread对象的InheritableThreadLocal变量。


    接着我们要从Thread类的源码查找头绪。

    Thread类源代码中,我们可以看到有这么2个成员变量:
    ```java
    ThreadLocal.ThreadLocalMap threadLocals = null;

    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    ```
    **如果是使用ThreadLocal创建线程变量,读写的是Thread对象的threadLocals;**
    **如果是使用InheritableThreadLocal创建线程变量,读写的是Thread对象的inheritableThreadLocals。**

    在Thread类的init方法可以看到(Thread所有构造方法都是调用init方法,这边仅贴出关键部分):
    ```java
    if (parent.inheritableThreadLocals != null)
                this.inheritableThreadLocals =
                    ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    ```
    ThreadLocal.createInheritedMap:
    ```java
    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }
    ```
    **如果父级线程的inheritableThreadLocals不为null,那么将父级线程的inheritableThreadLocals赋值到当前线程的inheritableThreadLocals变量。**

    总结:当使用InheritableThreadLocal创建线程变量时,父线程读写线程变量实际是写入父线程的inheritableThreadLocals中,在创建子线程时,会将父线程的inheritableThreadLocals复制给子线程的inheritableThreadLocals,子线程操作此线程变量时,也是读写自己线程的inheritableThreadLocals,这就达到了子线程可以获取父线程ThreadLocal的效果。

    # 其他要点

    - 如果使用了线程池,线程是会被复用的,因此线程的threadLocals和inheritableThreadLocals也会复用,在线程池使用ThreadLocal可能会产生一些问题,需要留意;
    - JDK本身提供创建线程池的方法,是不支持获得父级线程的ThreadLocal变量的。

     

  • 相关阅读:
    使用SpaceDesk实现iPad成为电脑拓展屏(保姆级教程)
    Linux学习-26-passwd命令:修改用户密码
    【Selenium】Selenium获取Network数据(高级版)
    0基础学习VR全景平台篇第120篇:极坐标处理接缝 - PS教程
    SAP 内码转外码
    金融领域:怎么保持电力系统连续供应?
    git的详细使用
    计算机毕业设计之java+SSM动物园门票预订网站系统
    mybatis拦截器 + 注解对敏感字段进行加密解密
    GC不懂?进来我们聊聊
  • 原文地址:https://www.cnblogs.com/ladderx/p/16859281.html