• 深入解析ThreadLocal 详解、实现原理、使用场景方法以及内存泄漏防范 多线程中篇(十七)


    简介

    从名称看,ThreadLocal 也就是thread和local的组合,也就是一个thread有一个local的变量副本
    ThreadLocal提供了线程的本地副本,也就是说每个线程将会拥有一个自己独立的变量副本
    方法简洁干练,类信息以及方法列表如下
    image_5c64cced_8d

    示例

    在测试类中定义了一个ThreadLocal变量,用于保存String类型数据
    创建了两个线程,分别设置值,读取值,移除后再次读取
    package test2;
    /**
    * Created by noteless on 2019/1/30. Description:
    */
    public class T21 {
    //定义ThreadLocal变量
    static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    public static void main(String[] args) {
    Thread thread1 = new Thread(() -> {
    //thread1中设置值
    threadLocal.set("this is thread1's local");
    //获取值
    System.out.println(Thread.currentThread().getName()+": threadLocal value:" + threadLocal.get());
    //移除值
    threadLocal.remove();
    //再次获取
    System.out.println(Thread.currentThread().getName()+": after remove threadLocal value:" + threadLocal.get());
    }, "thread1");
    Thread thread2 = new Thread(() -> {
    //thread2中设置值
    threadLocal.set("this is thread2's local");
    //获取值
    System.out.println(Thread.currentThread().getName()+": threadLocal value:" + threadLocal.get());
    //移除值
    threadLocal.remove();
    //再次获取
    System.out.println(Thread.currentThread().getName()+": after remove threadLocal value:" + threadLocal.get());
    }, "thread2");
    thread1.start();
    thread2.start();
    }
    }
    执行结果
    image_5c64cced_360f
    从结果可以看得到,每个线程中可以有自己独有的一份数据,互相没有影响
    remove之后,数据被清空
     
    从上面示例也可以看出来一个情况:
    如果两个线程同时对一个变量进行操作,互相之间是没有影响的,换句话说,这很显然并不是用来解决共享变量的一些并发问题,比如多线程的协作
    因为ThreadLocal的设计理念就是共享变私有,都已经私有了,还谈啥共享?
    比如之前的消息队列,生产者消费者的示例中
      final LinkedList<Message> messageQueue = new LinkedList<>();
    如果这个LinkedList是ThreadLocal的,生产者使用一个,消费者使用一个,还协作什么呢?
    但是共享变私有,如同并发变串行,或许适合解决一些场景的线程安全问题,因为看起来就如同没有共享变量了,不共享即安全,但是他并不是为了解决线程安全问题而存在的

    实现分析

    在Thread中有一个threadLocals变量,类型为ThreadLocal.ThreadLocalMap
    image_5c64cced_790c
    而ThreadLocalMap则是ThreadLocal的静态内部类,他是一个设计用来保存thread local 变量的自定义的hash map
    所有的操作方法都是私有的,也就是不对外暴露任何操作方法,也就是只能在ThreadLocal中使用了
    此处我们不深入,就简单理解为是一个hash map,用于保存键值对
    image_5c64ccee_227c
    也就是说Thread中有一个“hashMap”可以用来保存键值对

    set方法

    看一下ThreadLocal的set方法
    image_5c64ccee_3823
    在这个方法中,接受参数,类型为T的value
    首先获取当前线程,然后调用getMap(t)
    这个方法也很简单,就是直接返回Thread内部的那个“hashMap”(threadLocals是默认的访问权限)
    image_5c64ccee_45bc
    继续回到set方法,如果这个map不为空,那么以this为key,value为值,也就是ThreadLocal变量作为key
    如果map为空,那么进行给这个线程创建一个map ,并且将第一组值设置进去,key仍旧是这个ThreadLocal变量
    image_5c64ccee_63f9
    简言之:
    调用一个ThreadLocal的set方法,会将:以这个ThreadLocal类型的变量为key,参数为value的这一个键值对,保存在Thread内部的一个“hashMap”中

    get方法

    在get方法内部仍旧是获取当前线程的内部的这个“hashMap”,然后以当前对象this(ThreadLocal)作为key,进行值的获取
    image_5c64ccee_5c16
    我们对这两个方法换一个思路理解:
    每个线程可能运行过程中,可能会操作很多的ThreadLocal变量,怎么区分各自?
    直观的理解就是,我们想要获取某个线程的某个ThreadLocal变量的值
    一个很好的解决方法就是借助于Map进行保存,ThreadLocal变量作为key,local值作为value
    假设这个map名为:threadLocalsMap,可以提供setter和getter方法进行设置和读取,内部为
    • threadLocalsMap.set(ThreadLocal key,T value)
    • threadLocalsMap.get(ThreadLocal key)
    这样就是可以达到thread --- local的效果,但是是否存在一些使用不便?我们内部定义的是ThreadLocal变量,但是只是用来作为key的?是否直接通过ThreadLocal进行值的获取更加方便呢?
    怎么能够做到数据读取的倒置?因为毕竟值的确是保存在Thread中的
    其实也很简单,只需要内部进行转换就好了,对于下面两个方法,我们都需要 ThreadLocal key
    threadLocalsMap.set(ThreadLocal key,T value)
    threadLocalsMap.get(ThreadLocal key) 
    而这个key不就是这个ThreadLocal,不就是this 么
    所以:
    • ThreadLocal.set(T value)就内部调用threadLocalsMap.set(this,T value)
    • ThreadLocal.get()就内部调用threadLocalsMap.get(this) 
    所以总结下就是:
    • 每个Thread内部保存了一个"hashMap",key为ThreadLocal,这个线程操作了多少个ThreadLocal,就有多少个key
    • 你想获取一个ThreadLocal变量的值,就是ThreadLocal.get(),内部就是hashMap.get(this);
    • 你想设置一个ThreadLocal变量的值,就是ThreadLocal.set(T value),内部就是hashMap.set(this,value);
    关键只是在于内部的这个“hashMap”,ThreadLocal只是读写倒置的“壳”,可以更简洁易用的通过这个壳进行变量的读写
    “倒置”的纽带,就是getMap(t)方法

    remove方法

    image_5c64ccee_4373
    remove方法也是简单,当前线程,获取当前线程的hashMap,remove

    初始值

    再次回头看下get方法,如果第一次调用时,指定线程并没有threadLocals,或者根本都没有进行过set
    会发生什么?
    如下图所示,会调用setInitialValue方法
    image_5c64ccee_5b3
    在setInitialValue方法中,会调用initialValue方法获取初始值,如果该线程没有threadLocals那么会创建,如果有,会使用这个初始值构造这个ThreadLocal的键值对
    简单说,如果没有set过(或者压根内部的这个threadLocals就是null的),那么她返回的值就是初始值
    image_5c64ccee_5f9f
    这个内部的initialValue方法默认的返回null,所以一个ThreadLocal如果没有进行set操作,那么初始值为null
    image_5c64ccee_12c6
    如何进行初始值的设定?
    可以看得出来,这是一个protected方法,所以返回一个覆盖了这个方法的子类不就好了?在子类中实现初始值的设置
    在ThreadLocal中提供了一个内部类SuppliedThreadLocal,这个内部类接受一个函数式接口Supplier作为参数,通过Supplier的get方法获取初始值
    image_5c64ccee_dbd
    Supplier是一个典型的内置函数式接口,无入参,返回类型T,既然是函数式接口也就是可以直接使用Lambda表达式构造初始值了!!!
    image_5c64ccee_e57
    如何构造这个内部类,然后进而进行初始化参数的设置呢?
    提供了withInitial方法,这个方法的参数就是Supplier类型,可以看到,这个方法将入参,透传给SuppliedThreadLocal的构造方法,直接返回一个SuppliedThreadLocal
    换句话说,我们不是希望能够借助于ThreadLocal的子类,覆盖initialValue()方法,提供初始值吗?
    这个withInitial就是能够达成目标的一个方法!
    image_5c64ccee_73d2
    使用withInitial方法,创建具有初始值的ThreadLocal类型的变量,从结果可以看得出来,我们没有任何的设置,可以获取到值
    image_5c64ccee_1240
    稍作改动,增加了一次set和remove,从打印结果看得出来,set后,使用的值就是我们新设置的
    而一旦remove之后,那么仍旧会使用初始值
    image_5c64ccee_47c5
    注意:
    对于initialValue方法的覆盖,其实即使没有提供这个子类以及这个方法也都是可以的,因为本质是要返回一个子类,并且覆盖了这个方法
    我们可以自己做,也可以直接匿名类,如下所示:创建了一个ThreadLocal的子类,覆盖了initialValue方法

    ThreadLocal <类型 > threadLocalHolder =new ThreadLocal <类型> () {

    public 类型 initialValue() {

    return XXX;

    }

    };

    但是很显然,提供了子类和方法之后,我们就可以借助于Lambda表达式进行操作,更加简介

    总结:

    通过set方法可以进行值的设定
    通过get方法可以进行值的读取,如果没有进行过设置,那么将会返回null;如果使用了withInitial方法提供了初始值,将会返回初始值
    通过remove方法将会移除对该值的写入,再次调用get方法,如果使用了withInitial方法提供了初始值,将会返回初始值,否则返回null
    对于get方法,很显然如果没有提供初始值,返回值为null,在使用时是需要注意不要引起NPE异常
     
    ThreadLocal,thread  local,每个线程一份,到底是什么意思?
    他的意思是对于一个ThreadLocal类型变量,每个线程有一个对应的值,这个值的名字就是ThreadLocal类型变量的名字,值是我们set进去的变量
    但是如果set设置的是共享变量,那么ThreadLocal其实本质上还是同一个对象不是么?
    这句话如果有疑问的话,可以这么理解
    对于同一个ThreadLocal变量a,每个线程有一个map,map中都有一个键值对,key为a,值为你保存的值
    但是这个值,到底每个线程都是全新的?还是使用的同一个?这是你自己的问题了!!!
    ThreadLocal可以做到每个线程有一个独立的一份值,但是你非得使用共享变量将他们设置成一个,那ThreadLocal是不会保障的
    这就好比一个对象,有很多引用指向他,每个线程有一个独立的引用,但是对象根本还是只有一个
    所以,从这个角度更容易理解,为什么说ThreadLocal并不是为了解决线程安全问题而设计的,因为他并不会为线程安全做什么保障,他的能力是持有多个引用,这多个引用是否能保障是多个不同的对象,你来决策
    所以我们最开始说的,ThreadLocal会为每个线程创建一个变量副本的说法是不严谨的
    是他有这个能力做到这件事情,但是到底是什么对象,还是要看你set的是什么,set本身不会对你的值进行干涉
    不过我们通常就是在合适的场景下通过new对象创建,该对象在线程内使用,也不需要被别的线程访问
    如下图所示,你放进去的是一个共享变量,他们就是同一个对象
    image_5c64ccef_3c7b

    应用场景

    前面说过,对于之前生产者消费者的示例中,就不适合使用ThreadLocal,因为问题模型就是要多线程之间协作,而不是为了线程安全就将共享变量私有化
    比如,银行账户的存款和取款,如果借助于ThreadLocal创建了两个账户对象,就会有问题的,初始值500,明明又存进来1000块,可支配的总额还是500
    那ThreadLocal适合什么场景呢?
    既然是每个线程一个,自然是适合那种希望每个线程拥有一个的那种场景(好像是废话)
    一个线程中一个,也就是线程隔离,既然是一个线程一个,那么同一个线程中调用的方法也就是共享了,所以说,有时,ThreadLocal会被用来作为参数传递的工具
    因为它能够保障同一个线程中的值是唯一的,那么他就共享于所有的方法中,对于所有的方法来说,相当于一个全局变量了!
    所以可以用来同一个线程内全局参数传递
    不过要慎用,因为“全局变量”的使用对于维护性、易读性都是挑战,尤其是ThreadLocal这种线程隔离,但是方法共享的“全局变量”
    如何保障必然是独立的一个私有变量?
    对于ThreadLocal无初始化设置的变量,返回值为null
    所以可以进行判断,如果返回值为null,可以进行对象的创建,这样就可以保障每个线程有一个独立的,唯一的,特有的变量

    示例

    对于JavaWeb项目,大家都了解过Session
    ps:此处不对session展开介绍,打开浏览器输入网址,这就会建立一个session,关闭浏览器,session就失效了
    在这一个时间段内,一个用户的多个请求中,共享同一个session
    Session 保存了很多信息,有的需要通过 Session 获取信息,有些又需要修改 Session 的信息
    每个线程需要独立的session,而且很多地方都需要操作 Session,存在多方法共享 Session 的需求,所以session对象需要在多个方法中共享
    如果不使用 ThreadLocal,可以在每个线程内创建一个 Session对象,然后在多个方法中将他作为参数进行传递
    很显然,如果每次都显式的传递参数,繁琐易错
    这种场景就适合使用ThreadLocal
     
    下面的示例就模拟了多方法共享同一个session,但是线程间session隔离的示例
    public class T24 {
    /**
    * session变量定义
    */
    static ThreadLocal<Session> sessionThreadLocal = new ThreadLocal<>();
    /**
    * 获取session
    */
    static Session getSession() {
    if (null == sessionThreadLocal.get()) {
    sessionThreadLocal.set(new Session());
    }
    return sessionThreadLocal.get();
    }
    /**
    * 移除session
    */
    static void closeSession() {
    sessionThreadLocal.remove();
    }
    /**
    * 模拟一个调用session的方法
    */
    static void fun1(Session session) {
    }
    /**
    * 模拟一个调用session的方法
    */
    static void fun2(Session session) {
    }
    public static void main(String[] args) {
    new Thread(() -> {
    fun1(getSession());
    fun2(getSession());
    closeSession();
    }).start();
    }
    /**
    * 模拟一个session
    */
    static class Session {
    }
    }

    所以,ThreadLocal最根本的使用场景应该是:

    在每个线程希望有一个独有的变量时(这个变量还很可能需要在同一个线程内共享)
    避免每个线程还需要主动地去创建这个对象(如果还需要共享,也一并解决了参数来回传递的问题)
    换句话说就是,“如何优雅的解决:线程间隔离与线程内共享的问题”,而不是说用来解决乱七八糟的线程安全问题
    所以说如果有些场景你需要线程隔离,那么考虑ThreadLocal,而不是你有了什么线程安全问题需要解决,然后求助于ThreadLocal,这不是一回事
    既然能够线程内共享,自然的确是可以用来线程内全局传参,但是不要滥用
    再次注意:
    ThreadLocal只是具有这样的能力,是你能够做到每个线程一个独有变量,但是如果你set时,不是传递的new出来的新变量,也就只是理解成“每个线程不同的引用”,对象还是那个对象(有点像参数传递时的值传递,对于对象传递的就是引用)

    内存泄漏

    ThreadLocal很好地解决了线程数据隔离的问题,但是很显然,也引入了另一个空间问题
    如果线程数量很多,如果ThreadLocal类型的变量很多,将会占用非常大的空间
    而对于ThreadLocal本身来说,他只是作为key,数据并不会存储在它的内部,所以对于ThreadLocal
    ThreadLocalMap内部的这个Entity的key是弱引用
    image_5c64ccef_779
    如下图所示,实线表示强引用,虚线表示弱引用
    对于真实的值是保存在Thread里面的ThreadLocal.ThreadLocalMap threadLocals中的
    借助于内部的这个map,通过“壳”ThreadLocal变量的get,可以获取到这个map的真正的值,也就是说,当前线程中持有对真实值value的强引用
    而对于ThreadLocal变量本身,如下代码所示,栈中的变量与堆空间中的这个对象,也是强引用的
      static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    不过对于Entity来说,key是弱引用
    image_5c64ccef_3050
    当一系列的执行结束之后,ThreadLocal的强引用也会消亡,也就是堆与栈之间的从ThreadLocal Ref到ThreadLocal的箭头会断开
    由于Entity中,对于key是弱引用,所以ThreadLocal变量会被回收(GC时会回收弱引用)
    而对于线程来说,如果迟迟不结束,那么就会一直存在:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value的强引用,所以value迟迟得不到回收,就会可能导致内存泄漏 
    ThreadLocalMap的设计中已经考虑到这种情况,所以ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value
    以get方法为例
    image_5c64ccef_48c7
    一旦将value设置为null之后,就斩断了引用于真实内存之间的引用,就能够真正的释放空间,防止内存泄漏
    image_5c64ccef_751d
    但是这只是一种被动的方式,如果这些方法都没有被调用怎么办?
    而且现在对于多线程来说,都是使用线程池,那个线程很可能是与应用程序共生死的,怎么办?
    那你就每次使用完ThreadLocal变量之后,执行remove方法!!!!
    从以上分析也看得出来,由于ThreadLocalMap的生命周期跟Thread一样长,所以很可能导致内存泄漏,弱引用是相当于增加了一种防护手段
    通过key的弱引用,以及remove方法等内置逻辑,通过合理的处理,减少了内存泄漏的可能,如果不规范,就仍旧会导致内存泄漏

    总结

    ThreadLocal可以用来优雅的解决线程间隔离的对象,必须主动创建的问题,借助于ThreadLocal无需在线程中显式的创建对象,解决方案很优雅
    ThreadLocal中的set方法并不会保障的确是每个线程会获得不同的对象,你需要对逻辑进行一定的处理(比如上面的示例中的getSession方法,如果ThreadLocal 变量的get为null,那么new对象)
    是否真的能够做到线程隔离,还要看你自己的编码实现,不过如果是共享变量,你还放到ThreadLocal中干嘛?
    所以通常都是线程独有的对象,通过new创建
  • 相关阅读:
    Java类对象转json字符串,servlet或js的json字符串转json对象或数组
    大三下每周总结--第一周
    阅读架构漫谈九篇博客有感-1500字
    大三上寒假15天--第15天
    大三上寒假15天--第14天
    大三上寒假15天--第13天
    jenkins+appium android app自动化测试
    windows jenkins 卸载
    jenkins运行Python
    pytest+jenkins安装+allure导出报告
  • 原文地址:https://www.cnblogs.com/noteless/p/10373044.html
Copyright © 2020-2023  润新知