ThreadLocal

目录

  • ThreadLocal
  • ThreadLocalMap
    • ThreadLocalMap 核心方法
  • ThreadLocalMap的内存泄漏问题
  • 应用

ThreadLocal

  ThreadLocal是线程的一个本地化对象。当工作于多线程中的对象使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程分配一个独立的变量副本。所以每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。从线程的角度看,这个变量就像是线程的本地变量。
  提供了set和get方法来访问拷贝过来的变量副本。底层也是封装了ThreadLocalMap集合类来绑定当前线程和变量副本的关系,各个线程独立并且访问安全。

ThreadLocal 存储结构
  每个线程持有自己的 ThreadLocalMap,ThreadLocalMap 初始容量为16,在调用ThreadLocal 的 set 方法时,将以ThreadLocal的弱引用作为Key存储在本线程的ThreadLocalMap 里面,ThreadLocalMap的Value为Object 类型。ThreadLocal的set() 方法、get() 方法以及remove() 方法,最终都落到了ThreadLocalMap的头上。

ThreadLocalMap

  ThreadLocalMap 是ThreadLocal 内部的一个Map实现,数据结构采用数组 + 开放地址法。ThreadLocalMap的Entry继承WeakReference,以ThreadLocal的弱引用为key。虽然继承 WeakReference,但只能实现对 Reference 的 key 的回收,而对 value 的回收需要手动解决。当 map.get() = null 的时候将它称为 过期

ThreadLocalMap 核心方法

ThreadLocalMap 之 key 的 hashCode 计算

 private final int threadLocalHashCode = nextHashCode();
 private static AtomicInteger nextHashCode = new AtomicInteger();
 //1640531527 这是一个神奇的数字,能够让hash槽位分布相当均匀
 private static final int HASH_INCREMENT = 0x61c88647; 
 private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
 }

  在ThreadLocalMap中的hashCode全部使用 threadLocalHashCode 字段。threadLocalHashCode 用 final修饰,不可变。threadLocalHashCode 的生成调用 nextHashCode(),所有ThreadLocalMap 的 hashCode 使用静态的 AtomicInteger 每次增加1640531527 来产生。ThreadLocal 的 nextHashCode 是由 static 修饰的,他是一个共享变量,所有的 ThreadLocal 共享一个 AtomicInteger,在其基础上 CAS 增加。

ThreadLocalMap 之 set() 方法
  set 方法包含:开放地址法;hash算法:均匀的 hash 算法使其可以很好地配合开放地址法使用以及过期值清理

ThreadLocalMap 为什么采用开放地址法?
由于 ThreadLocalMap 的 hashCode 的精妙设计,使hash冲突很少,并且 Entry 继承 WeakReference, 很容易被回收并且开放地址可以节省一些指针空间。

对于过期值清理:
cleanSomeSlots
  当新元素被添加时或者另一个过期元素已被删除时,会调用cleanSomeSlots。该方法会试探性地扫描一些 entry,寻找过期的条目。它执行对数数量的扫描,此方法并没有真正的清除,只是找到了要清除的位置,而真正的清除在 expungeStaleEntry(int staleSlot) 里面。

expungeStaleEntry(int staleSlot)
  这里是真正的清除,不仅仅会清除当前过期的slot,还会往后查找直到遇到null的slot为止。

ThreadLocalMap 之 getEntry() 方法
getEntry() 主要是在 ThreadLocal 的 get() 方法里被调用
首先运算槽位 i ,然后判断 table[i] 是否是目标entry,不是则进入 getEntryAfterMiss(key, i, e);getEntryAfterMiss方法是在遇到 hash 冲突时往后继续查找,并且会清除查找路上遇到的过期slot。

ThreadLocalMap 之 rehash() 方法
  rehash() 里首先调用 expungeStaleEntries(),然后循环调用 expungeStaleEntry(j),此方法会清除所有过期的slot。继续看 resize():resize() 方法里也会过滤掉一些过期的 entry。

总结ThreadLocalMap 的 value清理触发时间:

  1. set(ThreadLocal key, Object value)
      若无hash冲突,则先向后检测log2(N)个位置,发现过期 slot 则清除,如果没有任何 slot 被清除,则判断 size >= threshold,超过阀值会进行 rehash(),rehash()会清除所有过期的value
  2. ThreadLocal 的 get() 方法调用getEntry(ThreadLocal key)
      如果没有直接在hash计算的 slot 中找到entry, 则需要向后继续查找(直到null为止),查找期间发现的过期 slot 会被清除
  3. remove(ThreadLocal key)
      remove 不仅会清除需要清除的 key,还会清除hash冲突的位置的已过期的 key;这里的清除并不代表被回收,只是把 key置为 null,value 的具体回收时间由垃圾收集器决定

ThreadLocalMap的内存泄漏问题

  ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部关联的强引用,那么在虚拟机进行垃圾回收时,这个ThreadLocal会被回收,这样,ThreadLocalMap中就会出现key为null的Entry,这些key对应的value就无法访问,但是value却存在一条从Current Thread过来的强引用链。因此,这个线程对象被GC回收,那些key为null对应的value才会被回收,但在线程对象不被回收的情况下,核心线程是一直在运行的,就可能出现内存泄露的问题。
  ThreadLocalMap的解决方案是在获取key对应的value时,会调用ThreadLocalMap的getEntry(ThreadLocal key)方法。通过key.threadLocalHashCode & (table.length - 1)来计算存储key的Entry的索引位置,然后判断对应的key是否存在,若存在,则返回其对应的value,否则,调用getEntryAfterMiss(ThreadLocal, int, Entry)方法。ThreadLocalMap采用线性探查的方式来处理哈希冲突,所以会有一个while循环去查找对应的key,在查找过程中,若发现key为null,即通过弱引用的key被回收了,会调用expungeStaleEntry(int)方法,此时,CurrentThread Ref不存在一条到Entry对象的强引用链,Entry到value对象也不存在强引用,那在程序运行期间,它们自然也就会被回收。expungeStaleEntry(int)方法的后续代码就是以线性探查的方式,调整后续Entry的位置,同时检查key的有效性。
  在ThreadLocalMap中的set()/getEntry()方法中,都会调用expungeStaleEntry(int)方法,但是如果既不需要添加value,也不需要获取value,还是有可能产生内存泄漏的。所以很多情况下需要使用者手动调用ThreadLocal的remove()函数,防止内存泄露。若对应的key存在,remove()方法也会调用expungeStaleEntry(int)方法,来删除对应的Entry和value。
  其实,最好的方式就是将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,可以防止内存泄露。

应用

  Java中常用的开源框架,Mybatis、Hibernate中涉及到线程通过数据库连接对象Connection,对其数据进行操作,都会使用ThreadLocal类来保证Java多线程程序访问和数据库数据的一致性问题。最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等

你可能感兴趣的:(Java)