Java并发成神之路-精通JUC并发工具十八般武艺——ThreadLocal(二)

两大使用场景——ThreadLocal的用途

  • 场景1:每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random)

    SimpleDateFormat为线程不安全的,如果在方法中每次都创建一个SimpleDateFormat对象
    如果访问量较高,会导致格式化出来的时间有部分是重复的
    可以通过使用ThreadLocal,针对每一个线程创建一个SimpleDateFormat对象,这样同一个线程的不同任务是排队进行的,不同线程使用的是不同的对象,就可以解决并发问题

  • 场景2:每个线程内需要保存全局变量(例如在拦截器中获取用户信息,可以让不同方法直接使用,避免参数传递的麻烦)

    假设当前用户信息需要被线程内所有方法共享,一个比较繁琐的解决发难就是把user作为阐述层层传递,从方法1到方法2到方法3,以次类推,但是这样做代码冗余,且不利于维护
    使用ThreadLocal,当前线程中公共变量,可以在当前线程中的任何一个方法中调用获取相应的变量值

ThreadLocal的两个作用

  • 让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象)
  • 在任何方法中都可以轻松的获取到对象

根据共享对象的生产时机不同,选择initialValueset来保存对象

  • 场景一:initialValue

    在ThreadLocal第一个get的时候,把对象给初始化出来,对象的初始化时机可以由我们控制

  • 场景二:set

    如果需要保存到ThreadLocal里面的对象的生产时机不由我们随意控制,例如拦截器生成的用户信息,用ThreadLocal.set()直接放到我们的ThreadLocal中去,以便后续使用

使用ThreadLocal带来的好处

  • 达到线程安全的目的
  • 不需要加锁,提高执行效率
  • 更高效的利用内存、节省开销:相比于每一个任务都新建一个SimpleDateFormat,显然用ThreadLocal可以节省内存和开销
  • 免去传参的繁琐:无论是场景一的工具类,还是场景二的用户名,都可以在任何地方直接通过ThreadLocal拿到,再也不需要每次都传同样的参数,ThreadLocal使得代码耦合度更低,更优雅

主要方法介绍

  • T initialValue()

    该方法会返回当前线程对应的”初始值“,这是一个延迟加载的方法,只有再调用get()的时候,才会触发
    当线程第一个吃用get()方法访问变量时,将调用此方法,除非线程先调用了set()方法,在这种情况下,不会为线程调用本initialValue()方法
    通常每个线程最调用一次此方法,但如果已经调用了remove(),再调用get(),则可以再次调用此方法
    如果不重写本方法,这个放回就会返回null,一般使用匿名内部类的方法来重写initialValue()方法,以便再后续使用中,可以初始化副本对象
    该方法没有默认实现,如果我们需要到该方法,需要自己实现,通常使用匿名内部类的方式

  • void set(T t)

    为这个线程设置一个新值

  • T get()

    得到这个线程对应的value,如果是首次调用get()(没有调用set()方法直接调用get()),则会调用initialValue()方法获取这个值
    get()方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传进去,取出map中属于本ThreadLocal的value
    注意,这个map以及map中的key和value都是保存再线程中的,而不是保存再ThreadLocal中的

  • void remove()

    删除对应这个线程的值

ThreadLocalMap类

  • ThreadLocalMap类是每个线程Thread类里面的变量,里面最重要的是一个键值对数组Entry[] table,可以认为是一个map,键值对:
    • 键:这个ThreadLocal
    • 值:实际需要的成员变量,比如user或者SimpleDateFormat对象
  • 当发现hash冲突的时候,ThreadLocalMap采用的是线性探测法,也就是如果发生冲突,就继续找下一个空位置,而不是采用链表拉链

ThreadLocal注意点

内存泄漏

  • 什么是内存泄漏:某个对象不再有用,但是占用的内存却不能被回收
  • key的泄漏:ThreadLocalMap中的Entry继承自WeakReference,是弱引用
    • 弱引用的特点是,如果这个对象只被弱引用关联(没有任何强引用关联),那么这个对象就可以被回收,所以弱引用不会阻止GC
  • value的泄漏
    • ThreadLocalMap的每个Entry都是一个对key的弱引用,同时,每个Entry都包含了一个对value的强引用
    • 正常情况下,当线程终止,保存再ThreadLocal里面的value会被垃圾回收,因为没有任何强引用了
    • 但是如果线程不终止(比如线程需要保持很久,使用线程池的场景,线程一直被重复使用),那么key对应的value就不能被回收,因为又以下的调用链:
      • Thread -> ThreadLocalMap -> Entry(key为null) -> value
    • 因为value和Thread之间还存在这个强引用链路,所以导致value无法回收,就可能出现OOM
    • JDK已经考虑到了这个问题,多以在set,remove,rehash方法中会扫描key为null的Entry,并把相应的value设置成null,这样value对象就可以被回收
    • 但是如果一个ThreadLocal不被使用,那么实际上set,remove,rehash方法也不会被调用到,如果同时线程又不停止,那么调用链就一直存在,那么就导致了value的内存泄漏

如何避免内存泄漏(阿里规约)

  • 调用remove方法,就会删除相应的Entry对象,可以避免内存泄漏,所以使用完ThreadLocal之后,应该调用remove方法

空指针问题

  • 当ThreadLocal.get()方法出来的值,直接被基本类型接受的时候,如果值是空,那么在转为基本类型的时候会抛出控制在异常

共享对象

  • 如果每个线程中ThreadLocal.set()进去的东西本来就是多个线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get()取的还是这个共享对象本身,还是会有并发访问的问题

如果使用ThreadLocal就可以解决问题,那么不要强制使用

  • 例如在任务数很少的时候,在局部变量中可以新建对象就可以解决问题,那么就不需要使用到ThreadLocal

优先使用框架的支持,而不是自己创造

  • 例如spring中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为自己可能会忘记调用remove()方法,造成内存泄漏