ThreadLocal

使用场景

  1. 每个线程需要一个独享的变量(通常是工具类,典型需要使用了类有SimpleDataFormat和Random)
  2. 每个线程需要保存全局变量(如在拦截器中获取哦那个户的信息),可以让不同的方法直接使用,避免传递参数的麻烦

场景一,每个线程需要独享的对象

  • 每个Thread内有自己的实例副本,不共享
  • 见代码

场景二 避免传递参数

截屏2021-03-07 下午4.07.52
  • 见代码

使用ThreadLocal有哪些好处

  • 达到线程安全
  • 不用加锁,执行效率高
  • 更高效的节省内存,减小开销
  • 免去传参的繁琐,降低代码耦和

ThreadLocal 原理

ThreadLocal原理图
  • 每个Thread会持有一个ThreadLocalMap(一对一关系)
  • 一个ThreadLocalMap存储多个ThreadLocal对

主要方法介绍

  • T initialValue()
    • 返回当前线程对应的初始值,这是一个延迟加载,只有调用get()后才会触发
    • 当线程第一次使用get()方法访问变量时将调用此方法,除非在此之前先用过了set()方法,则不会调用initialValue()
    • 通常线程只调用一次此方法就够了,单数如果调用了remove()再用get()就会重新调用此方法
  • void set(T t)
    • 为这个线程设置一个新值
  • T get()
    • 得到这个线程对应的值,如果首次调用,会使用initialValue()来获取值
  • void remove()
    • 删除这个线程对应的值

ThreadLocalMap类

ThreadLocalMap 即 Thread.threadlocals

  • ThreadLocalMap类是每个Thread类里面的变量,里面最重要的是一个键值对数组Entry[] table 可以认为是一个map,键值对:

    • 键:这个ThreadLocal
    • 值:实际需要的成员变量
  • Entry[] table类似于hashmap,但是实际处理还是有不同

    • 冲突:HashMap用链表或红黑树解决冲突,ThreadLocalMap采用线性探测法,如果冲突就找下一个位置

Value的泄露

ThreadLocalMap的每个Entry都是对key弱引用,同时,对value强引用

截屏2021-03-07 下午5.10.00
  • 因为value和Thread之间的强引用还存在,所以value无法被回收,就可能导致oom
  • JDK已经考虑到这个问题,所以在set(), remove(), rehash()等方法中会扫描key为null的entry,并把对应的vale设为null,这样value对象就可以被回收
  • 但是如果一个ThreadLocal不被使用,则其方法也不会被调用,同时线程又不停止的话,就会发生内存泄露

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

  • 调用remove方法就可以删除对应的entry对象,可以避免内存泄露,所以使用完ThreadLocal后应该调用remove方法
  • 如果是拦截器的方法,那么拦截请求时创建,也应该拦截请求退出前销毁

注意空指针异常

如声明一个Long的ThreadLocal,还没有set就get,同时想作为long直接返回,就会发生空指针异常,因为在Long到long到拆箱过程中出错了。

注意共享对象

如果放进ThreadLocal的是一个共享变量(如static),那么多个线程取得得还是那个共享变量,还是会有并发访问问题

优先使用框架支持的

例如在Spring中,可以使用RequestContextHolder、DateTimeContextHolder