• ThreadLocal详解


    ThreadLocal简介

    通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK 中提供的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

    如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get()set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

    再举个简单的例子:

    比如有两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么 ThreadLocal 就是用来避免这两个线程竞争的

    ThreadLocal示例

    public class Test06 {
        public static void main(String[] args) {
            for (int i = 0; i < 5; i++) {
                ThreadLocalExample threadLocalExample = new ThreadLocalExample();
                Thread thread = new Thread(threadLocalExample, "Thread---" + i);
                try {
                    Thread.sleep(new Random().nextInt(100));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                thread.start();
            }
        }
    }
    
    class ThreadLocalExample implements Runnable{
        ThreadLocal<SimpleDateFormat> format = ThreadLocal.withInitial(()->new SimpleDateFormat("YYYY-mm-dd HH:mm:ss"));
    
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"  default: "+format.get().toPattern());
            try {
                TimeUnit.MILLISECONDS.sleep(new Random().nextInt(100));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            format.set(new SimpleDateFormat());
            System.out.println(Thread.currentThread().getName()+"  new: "+ format.get().toPattern());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    Thread---0  default: YYYY-mm-dd HH:mm:ss
    Thread---0  new: yy-M-d ah:mm
    Thread---1  default: YYYY-mm-dd HH:mm:ss
    Thread---1  new: yy-M-d ah:mm
    Thread---2  default: YYYY-mm-dd HH:mm:ss
    Thread---3  default: YYYY-mm-dd HH:mm:ss
    Thread---4  default: YYYY-mm-dd HH:mm:ss
    Thread---4  new: yy-M-d ah:mm
    Thread---3  new: yy-M-d ah:mm
    Thread---2  new: yy-M-d ah:mm
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    从输出中可以看出,Thread-0 已经改变了 formatter 的值,但仍然是 thread-2 默认格式化程序与初始化值相同,其他线程也一样。

    ThreadLocal原理

    Thread源码中:

    public class Thread implements Runnable {
        //......
        //与此线程有关的ThreadLocal值。由ThreadLocal类维护
        ThreadLocal.ThreadLocalMap threadLocals = null;
    
        //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
        ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
        //......
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    从上面Thread类 源代码可以看出Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 setget方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()set()方法。

    其实每个线程的本地变量不是存放在ThreadLocal实例里面,而是存放在调用线程的threadLocals变量里面。也就是说, ThreadLocal类型的本地变量存放在具体的线程内存空间中。ThreadLocal就是一个工具壳,它通过set方法把value值放入调用线程的threadLocals里面并存放起来,当调用线程调用它的get方法时,再从当前线程的threadLocals变量里面将其拿出来使用。如果调用线程一直不终止, 那么这个本地变量会一直存放在调用线程的threadLocals变量里面,所以当不需要使用本地变量时可以通过调用ThreadLocal变量的remove方法,从当前线程的threadLocals里面删除该本地变量。另外, Thread里面的threadLocals为何被设计为map结构?很明显是因为每个线程可以关联多个ThreadLocal变量。

    下面简单分析ThreadLocal 的 set get remove 方法的实现逻辑

     /**
         * Sets the current thread's copy of this thread-local variable
         * to the specified value.  Most subclasses will have no need to
         * override this method, relying solely on the {@link #initialValue}
         * method to set the values of thread-locals.
         *
         * @param value the value to be stored in the current thread's copy of
         *        this thread-local.
         */
        public void set(T value) {
            //(1)获取当前线程
            Thread t = Thread.currentThread();
            //(2)将当前线程作为key,去查找对应的线程变量,找到则设置
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else{
                //(3)第一次调用就创建当前线程对应的HashMap
                 createMap(t, value);
            }
        }
    	/*代码(1)首先获取调用线程,然后使用当前线程作为参数调用getMap(t)方法,getMap(Thread t)的代码如下。
    	可以看到, getMap(t)的作用是获取线程自己的变量threadLocals, threadlocals变量被绑定到了线程的成员变量上。如果getMap(t)的返回值不为空,则把value值设置到threadLocals中,也就是把当前·变量值放入当前线程的内存变量threadLocals中。threadLocals是一个HashMap结构,其中key就是当前ThreadLocal的实例对象引用, value是通过set方法传递的值。
    	**/
    	ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }
    /*
    如果getMap(t)返回空值则说明是第一次调用set方法,这时创建当前线程的threadLocals变量。下面来看createMap(t, value)做什么。  创建了threadLocals 变量
    **/
     	void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    /**
         * Returns the value in the current thread's copy of this
         * thread-local variable.  If the variable has no value for the
         * current thread, it is first initialized to the value returned
         * by an invocation of the {@link #initialValue} method.
         *
         * @return the current thread's value of this thread-local
         */
        public T get() {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                //不为null 直接返回数据
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;
                    return result;
                }
            }
            //为空 那么则初始化当前线程的threadLocals成员交量
            return setInitialValue();
        }
    //首先获取当前线程实例,如果当前线程的threadLocals变量不为null,则直·接返回当前线程绑定的本地变量,否则执行代码进行初始化。setInitialValue)的代码如下。
    	/**
         * Variant of set() to establish initialValue. Used instead
         * of set() in case user has overridden the set() method.
         *
         * @return the initial value
         */
        private T setInitialValue() {
            T value = initialValue();
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
            return value;
        }
    	protected T initialValue() {
            return null;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    public void remove() {
             ThreadLocalMap m = getMap(Thread.currentThread());
             if (m != null)
                 m.remove(this);
         }
    //如以上代码所示,如果当前线程的threadLocals变量不为空,则删除当前线程中指定ThreadLocal实例的本地变量。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    总结: 在每个线程内部都有一个名为threadLocals的成员变量,该变量的类型为HashMap,其中key为我们定义的ThreadLocal变量的this引用, value则为我们使用set方法设置的值。

    每个线程的本地变量存放在线程自己的内存变量threadLocals中 如果当前线程一直不消亡,那么这些本地变量会一直存在,所以可能会造成内存溢出, 因此使用完毕后要记得调用ThreadLocal的remove方法删除对应线程的threadLocals中的本地变量

    每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。

    ThreadLocalRandom,就是借鉴ThreadLocal的思想实现的

    比如我们在同一个线程中声明了两个 ThreadLocal 对象的话,会使用 Thread内部都是使用仅有那个ThreadLocalMap 存放数据的,ThreadLocalMap的 key 就是 ThreadLocal对象,value 就是 ThreadLocal 对象调用set方法设置的值。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bmYAmJgH-1660577315500)(https://b3logfile.com/file/2021/07/image-f4535ee9.png)]

    ThreadLocalMapThreadLocal的静态内部类。

    image.png

    注意点

    ThreadLocal不支持继承

    package com.pingfa.demo.course;
    
    /**
     * @Author: gyy
     * @Description:
     * @Date: 2021/6/9 12:04
     */
    public class ThreadLocalTest {
        static final  ThreadLocal<String> threadLocal = new ThreadLocal<>();
    
        public static void main(String[] args) {
            threadLocal.set("main set value");
    
            new Thread(()->{
                System.out.println("testThread:"+threadLocal.get());
            },"testThread").start();
    
            System.out.println("mainThread: "+threadLocal.get());
        }
    }
    //结果:
    mainThread: main set value
    testThread:null
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    也就是说,同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。根据.上面的介绍,这应该是正常现象,因为在子线程thread里面调用get方法时当前线程为thread线程,而这里调用set方法设置线程变量的是main线程,两者是不同的线程,自然子线程访问时返回null。那么有没有办法让子线程能访问到父线程中的值?答案是有。

    InheritableThreadLocal类

    为了解决上面的问题(在子线程中的获取不到父线程的ThreadLocal)InheritableThreadLocal继承ThreadLocal,其提供了一个特性,就是可以让子线程可以访问到父线程中设置的本地变量。

    InheritableThreadLocal 类源码:

    //由以下代码可知, InheritableThreadLocal继承了ThreadLocal,并重写了三个方法。由代码(3)可知, InheritableThreadLocal重写了createMap方法,那么现在当第一次调用set方法时,创建的是当前线程的inheritableThreadLocals变量的实例而不再是threadLocals。由代码(2)可知,当调用getMap方法获取当前线程内部的map变量时,获取的是inheritableThreadLocals而不再是threadLocals.
    public class InheritableThreadLocal<T> extends ThreadLocal<T> {
        /**
         *(1)
         */
        protected T childValue(T parentValue) {
            return parentValue;
        }
    
        /**
         * (2)
         */
        ThreadLocalMap getMap(Thread t) {
           return t.inheritableThreadLocals;
        }
    
        /**
         * (3)
         */
        void createMap(Thread t, T firstValue) {
            t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    综上可知,在InheritableThreadLocal的世界里,变量inheritableThreadLocals替代了threadLocals.下面我们看一下重写的代码(1)何时执行,以及如何让子线程可以访问父线程的本地变量。这要从创建Thread的代码说起,打开Thread类的默认构造函数,代码如下。

    image.png

    总结:InheritableThreadLocal 类通过重写代码(2)和(3) 让本地变量保存到具体线程的inheritableThreadLocals变量里面,那么线程在通过InheritableThreadLocal类实例,的set或者get方法设置变量时,就会创建当前线程的inheritableThreadLocals变量。当父.线程创建子线程时,构造函数会把父线程中inheritableThreadLocals变量里面的本地变量复制一份保存到子线程的inheritableThreadLocals变量里面。

    package com.pingfa.demo.course;
    
    /**
     * @Author: gyy
     * @Description:
     * @Date: 2021/6/9 17:07
     */
    public class InheritableThreadLocalTest {
        static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
    
        public static void main(String[] args) {
            threadLocal.set("Value");
    
            new Thread(()->System.out.println("child:"+threadLocal.get()),"child").start();
    
            System.out.println("main:"+threadLocal.get());
        }
    }
    //结果:
    main:Value
    child:Value
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    那么在什么情况下需要子线程可以获取父线程的threadlocal变量呢?情况还是蛮多的, 比如子线程需要使用存放在threadlocal变量中的用户登录信息,再比如一些中间件需要把统一的id追踪的整个调用链路记录下来。其实子线程使用父线程中的threadlocal方法有多种方式, 比如创建线程时传入父线程中的变量,并将其复制到子线程中,或者在父线程中构造一个map作为参数传递给子线程,但是这些都改变了我们的使用习惯,所以在这些情况下InheritableThreadLocal就显得比较有用。

    ThreadLocal内存泄漏问题

    ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()get()remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法

    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;
    
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    弱引用介绍:

    如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。

    弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。

  • 相关阅读:
    多御安全浏览器更新,这3个设置让你体验感提升
    嵌入式软件架构设计-建立抽象层
    研发效能之技术治理&技术治理架构师
    CSS学习基础知识
    Solidity学习-投票合约示例
    判断一份好的问卷有哪些标准?
    如何在Vue项目中,通过点击DOM自动定位VScode中的代码行?
    Qt执行带参sql
    【微信小程序】新版获取用户头像昵称(uniapp)(完整版附源码)
    白银期货开户交割规则有哪些?
  • 原文地址:https://blog.csdn.net/m0_46669446/article/details/126356843