• 【并发编程】ThreadLocal详解


    1.ThreadLocal简介
    • 多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。
    • ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。

    在这里插入图片描述

    2.ThreadLocal的简单使用
    • 启用两个线程分别对自己工作空间的num进行++
    public class ThreadLocalDemo {
    
        private ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(()->0);
    
        public void add(){
            threadLocal.set(threadLocal.get()+1);
        }
    
        public Integer get(){
            return threadLocal.get();
        }
    
        public static void main(String[] args) {
            ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
            for (int i = 0; i < 2; i++) {
                new Thread(()->{
                    while (true){
                        try {
                            Thread.sleep(5000L);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        threadLocalDemo.add();
                        System.out.println(Thread.currentThread().getName()+":线程当前获取num值为:"+threadLocalDemo.get());
                    }
                }).start();
            }
        }
    
    }
    
    • 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
    • 运行结果

    在这里插入图片描述

    3.ThreadLocal的实现原理
    • 下面是ThreadLocal的类图结构,从图中可知:Thread类中有两个变量threadlocals和inheritableThreadLocals,二者都是ThreadLocal内部类ThreadLocalMap类型变量,我们通过查看内部类ThreadLocalMap可以发现实际上它类似于一个HashMap。在默认情况下,每个线程中的两个变量都是null。
    class Thread implements Runnable {
        ThreadLocal.ThreadLocalMap threadLocals = null;
        ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 只有当线程第一次调用ThreadLocal的set或者get方法的时候才会创建他们。除此之外,每个线程的本地变量不是存放在ThreadLocal实例中,而是存放在调用线程的Thread实例的threadLocals变量里面,也就是说ThreadLocal类型的本地变量是存放在具体的线程空间上,其本身相当于一个装载线程本地变量的容器,通过set方法将value添加到调用线程的threadLocals中,当调用线程调用get方法的时候,从它的threadLocals中取出变量。如果调用线程一直不终止,那么这个本地变量将会一直存放在它的threadLocals中,所以不使用本地变量的时候调用remove()方法,将threadLocals中删除不使用的变量。
    • 类关系图

    在这里插入图片描述

    • set方法源码分析
    public void set(T value){
        //先获取当前线程实例(调用者线程)
        Thread t = Thread.currentThread();
        //先通过当前线程实例作为key去获取ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //如果获取不是null,直接set值,key为当前定义的ThreadLocal变量的this引用。
        if(map !=null )
            map.set(this,value);
        //如果为null说明是首次添加,要先创建出对应的map
        else
            createMap(t,value);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    ​ getMap(Thread t)方法获取的是当前线程对应的threadLocals。

    ThreadLocalMap getMap(Thread t){
        //获取的是自己的线程变量threadLocals,并且绑定到调用线程的的成员变量threadLocals上。
        return t.threadLocals; 
    }
    
    • 1
    • 2
    • 3
    • 4

    ​ 如果调用getMap方法返回null,就直接将value值设置到threadLocals中(key为当前线程的引用,值为本地变量);如果getMap方法返回null说明是第一次调用set方法(因为threadLoclas默认值为null),这个时候就要调用createMap方法创建threadLocals。createMap方法不仅创建了threadLocals,同时也将本地变量添加到threadLocals中。

    void createMap(Thread t,T firstValue){
        t.threadLocals = new ThreadLocalMap(this,firstValue);
    }
    
    • 1
    • 2
    • 3
    • get方法源码分析

    ​ 在get方法的实现中,首先获取当前调用者线程,如果当前线程的threadLocals不为null,就直接返回当前线程绑定的本地变量的值,否则执行setInitialValue方法初始化threadLocals变量。setInitialValue方法中,类似于set方法的实现,都是判断当前线程的threadLocals变量是否为null,是则添加本地变量(这个时候由于初始化,所以添加的值也为null),否则创建threadLocals变量,同样添加的值为null。

    public T get(){
        //获取当前线程实例
        Thread t = Thread.currentThread();
        //获取当前实例的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //判断map是否为空
        if(map != null){
            //获取map中的value返回
            ThreadLocalMap.Entry e = map.getEntry(this);
            if(e != null){
                T result = (T)e.value;
                return result;
            }
        }
        //如果map为空则调用setInitialValue进行初始化,设置threadLocals变量value设为null
        return setInitialValue();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    private T setInitialValue(){
        //protected T initialValue() { return null; }
        T value = initialValue();
        //获取当前线程
        Thread t = Thread.currentThread();
        //以当前线程作为key值,查找对应的线程变量,找到对应的map
        ThreadLocalMap map = getMap(t);
        //如果map不为null
        if(map != null)
            //这块value 的值为null
            map.set(this,value);
        else
            //这块value的值也为null
           	createMap(t,value);
        return value;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • remove方法源码分析

      remove方法判断该当前线程对应的threadLocals变量是否为null,不为null就直接删除当前线程中指定的threadLocals变量。

    public void remove(){
        //获取当前线程绑定的threadLocals
        ThreadLocalMap map = getMap(Thread.currentThread());
        //如果map不为null,就移除当前线程中指定的ThreadLocal实例的本地变量
        if(m != null){
            m.remove(this);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • Threadlocal的内部结构图

    在这里插入图片描述

    • 从上面的我们可以看出:
      • 每一个Thread线程内部都有一个Map类型为ThreadLocalMap。
      • Map里面存储线程本地对象(key)和线程的变量副本(value)。
      • Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。对于不同的线程,每次获取副本值时,别的线程并不能获取当前西安城的副本值,于是形成了副本的隔离互相不干扰。
      • ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,Map内部节点对象Entry也独立实现。
      • 每个ThreadLocal只能保存一个变量副本,如果想要上线一个线程能够保存多个副本,就要多创建几个ThreadLocal实例。
      • Threadlocal里存储的可以是long,也可以是其他对象,同一个线程里不断set会不断覆盖。不同线程间数据互不影响。
    • ThreadLocalMap源码中的set方法

    在这里插入图片描述

    ​ 每个线程内部都会有一个名为threadLocals的成员变量,该变量的类型为ThreadLocal.ThreadLocalMap类型,其中的key为当前定义的ThreadLocal变量的this引用,value为我们使用set方法设置的值,每个线程本地变量存放在自己的本地内存变量threadLocals中,如果当前线程一直不消亡,那么这些线程一直存在(所以会导致内存溢出),因此使用完毕后要将其remove掉。

    在这里插入图片描述

    4.ThreadLocal不支持继承性
    • 同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。(threadLocals中为当前调用线程对应的本地变量,所以二者自然是不能共享的
    public class ThreadLocalDemo {
        private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    
        public static void main(String[] args) {
    
            threadLocal.set("mainValue");
    
            new Thread(()->{
                System.out.println("子线程拿到threadLocal中的值:"+threadLocal.get());
            }).start();
    
            System.out.println("main线程拿到threadLocal中的值:"+threadLocal.get());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里插入图片描述

    5.InheritableThreadLocal支持继承性
    • 在上面说到的ThreadLocal类是不能提供子线程访问父线程的本地变量的,而InheritableThreadLocal类则可以做到这个功能.
    • 同样是上面的代码,将ThreadLocal改成InheritableThreadLocal即可

    在这里插入图片描述

    6.从ThreadLocalMap看ThreadLocal使用不当的内存泄漏问题
    • 首先我们先来看一下Java的四种引用用类型。
      • 强引用:Java中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被GC。
      • 软引用:如果一个对象持有软引用,那么在JVM发生OOM之前,是不会GC这个对象的,只有到JVM内存内存不足的时候才会GC这个对象,软引用和一个引用队列联合使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列中。
      • 弱引用:弱引用对象指挥生存到下一次GC之前,只要发生GC无论内存够不够,都会把弱引用对象GC掉。
      • 虚引用:虚引用是所有引用中最弱的一个,其存在就是将关联虚引用的对象在被GC掉之后收到一个通知。
    • 分析ThreadLocalMap内部实现
      • ThreadLocalMap内部实现其实就是一个Entry[]数组,分析Entry内部类。
    /**
     * 是继承自WeakReference的一个类,该类中实际存放的key是
     * 指向ThreadLocal的弱引用和与之对应的value值(该value值
     * 就是通过ThreadLocal的set方法传递过来的值)
     * 由于是弱引用,当get方法返回null的时候意味着坑能引用
     */
     static class Entry extends WeakReference<ThreadLocal<?>> {
          /** value就是和ThreadLocal绑定的 */
          Object value;
     
         //k:ThreadLocal的引用,被传递给WeakReference的构造方法
         Entry(ThreadLocal<?> k, Object v) {
             super(k);
             value = v;
         }
     }
     //WeakReference构造方法(public class WeakReference extends Reference )
     public WeakReference(T referent) {
         super(referent); //referent:ThreadLocal的引用
     }
     
     //Reference构造方法      
     Reference(T referent) {
         this(referent, null);//referent:ThreadLocal的引用
     }
     
     Reference(T referent, ReferenceQueue<? super T> queue) {
         this.referent = referent;
         this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
     }
    
    • 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

    ​ Entry的源码中我们可以看出Entry继承WeakReference>类,当前的ThreadLocal的应用key被传递到WeakReference>的构造函数中,所以ThreadLocalMap中的key为ThreadLocal的弱引用,value就是通过ThreadLocal设置的值,,如果当前线程一直没由调用该ThreadLocal 的remove方法,这时还有别的地方对ThreadLocal的引用,那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,由于ThreadLocalMap中key为ThreadlLocal的弱引用,当下一次GC的时候,key会被释放,但是value不会,这时就会造成key为空,但是value不为空的情况,造成内存泄露,所以实际开发中应尽量漏掉remove方法。

  • 相关阅读:
    测试人员的价值体现在哪里
    Shadowing Japanese 中上 Unit 1
    法制博览杂志法制博览杂志社法制博览编辑部2022年第24期目录
    QT修改windowTitle的名字以及图片
    redis 多租户隔离 ACL 权限控制(redis-cli / nodejs的ioredis )
    JavaScript 数据类型及其用法
    c#常用的数据结构
    ImportError: /lib/libgdal.so.26: undefined symbol: sqlite3_column_table_name
    MySQL(一)基本语句(列排名、去掉重复行、运算符与分页排序)详解
    初识动态内存分配
  • 原文地址:https://blog.csdn.net/weixin_47533244/article/details/127802395