ThreadLocal 有感而发

601 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情

ThreadLocal 的一些小tips

特殊的疑问

  1. 线程里面直接提供一个map,通过key,value操作不更好吗?
  2. 为什么ThreadLocal变量一般是static的?
  3. ThreadLocalMap为什么要设计为成ThreadLocal的内部类,而不直接定义在Thread中?
  4. ThreadLocalMap的hash冲突解决方式为什么要设计为开放寻址法?
  5. ThreadLocalMap的弱引用的作用?
key的局限性:
  1. 对于固定类型的key,局限性太高,如果采用Object类型声明又存在类型转换的问题.
  2. 对于线程池而言,单个线程的生命周期与整个应用的声明周期一致,如果某个key不在使用,那么他的value将一直保留,不会被gc;

image.png

ThreadLocal 为什么一般定义为static的。

首先static变量意味着可以直接通过class来访问,而不需要经过new一个新的对象来访问. 假设不同static标识,那么每次都new一个对象,都会存在一个新的ThreadLocal对象,与之前的不是同一个,进而无法操作原来的map的值;或许有人说:我们为什么不采用单例模式来取处理呢? 当然单例模式也能解决问题,实现单例的方式有很多种:

  1. 饿汉模式
  2. 懒汉模式
  3. 枚举模式
  4. 静态内部类
  5. spring基于容器的单例 所以用单例实现没问题,但是个人感觉这是简单问题复杂化了.直接用static标识进而省去了哪些麻烦;
ThreadLocalMap为什么要设计为再ThreadLocal的内部类

将ThreadLocalMap定义在Thread类内部看起来更符合逻辑,但是ThreadLocalMap并不需要Thread对象来操作,所以定义在Thread类内只会增加一些不必要的开销。定义在ThreadLocal类中的原因是ThreadLocal类负责ThreadLocalMap的创建,仅当线程中设置第一个ThreadLocal时,才为当前线程创建ThreadLocalMap,之后所有其他ThreadLocal变量将使用一个ThreadLocalMap。 总的来说就是,ThreadLocalMap不是必需品,定义在Thread中增加了成本,定义在ThreadLocal中按需创建。

ThreadLocalMap的hash冲突解决为开放寻址法
hash算法:

把任意长度的输入,通过指定算法变换成固定长度的输出,这个输出就是Hash值,对应的算法成为hash算法; 常用的hash算法有MD5、SHA-1、SHA-2等;

hash冲突的解决方式
  1. 链地址法:
  2. 开放寻址法
  3. 建立公共溢出: 将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。
  4. 再hash法: 产生冲突时计算另一个哈希函数地址,直到不再产生冲突为止: 有这么多解决hash冲突的算法,那为什么ThreadLocalMap要采用开放寻址法这种效率比较地下的算法呢? 我的理解是ThreadLocalMap针对线程而言, 每个线程相互隔离,存储的元素相对较少, 不会涉及大面积的hash冲突,而且ThreadLocal的hash值设计的也比较巧妙:
private final int threadLocalHashCode = nextHashCode();

/**
 * The next hash code to be given out. Updated atomically. Starts at
 * zero.
 */
private static AtomicInteger nextHashCode =
    new AtomicInteger();

/**
 * The difference between successively generated hash codes - turns
 * implicit sequential thread-local IDs into near-optimally spread
 * multiplicative hash values for power-of-two-sized tables.
 */
private static final int HASH_INCREMENT = 0x61c88647;

/**
 * Returns the next hash code.
 */
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

以上代码就是其hash值得计算方式: threadLocalHashCode方法最终调用的是nextHashCode()方法。而nextHashCode()方法如下面代码所示调用的是getAndAdd,这个方法的作用是让当前线程的nextHashCode这个值与魔法值HASH_INCREMENT相加。每调用一次加一次魔法值。也就是线程中每添加一个threadlocal,AtomicInteger 类型的nextHashCode值就会增加一个魔法值HASH_INCREMENT。而魔法值HASH_INCREMENT和斐波那契散列有关(这是一种乘数散列法,只不过这个乘数比较特殊,是32位整型上限2^32-1乘以黄金分割比例0.618…的值2654435769,用有符号整型表示就是-1640531527,去掉符号后16进制表示为0x61c88647),其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里, 也就是Entry[] table中,这样做可以尽量避免hash冲突;

ThreadLocalMap的弱引用
java中引用的分类
  1. 强引用: 把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个
    对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的

  2. 软引用: 软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回
    收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。

  3. 弱引用 弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只
    要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存

  4. 虚拟引用 虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要
    作用是跟踪对象被垃圾回收的状态。

ThreadLocal弱引用被回收

由于线程池中的线程生命周期很长,一般不会被主动回收.我们定义的key为ThreadLocal对象,它对应的value很大时,如果key为强引用,那么他们不会被回收,一直占据着内存(已成无效对象). 那么再弱引用情况下; 如果我们手动把static变量指向的ThreadLocal手动置为null;那么key1不在指向, 此时仅存在一个弱引用指向这个对象,按照弱引用回收的流程,触发下次垃圾回收时,其所占据的内存空间将被释放.对应的value占据的空间也被回收.

image.png