ThreadLocal彻底搞懂

前言

ThreadLocal的实现机制不复杂,它将自身实例作为key,和需要保存的value一起存入到当前线程的一个map当中,代码可以简单的描写为(当然实际的代码并不是这样):

Thread.currentThread().threadLocals.put(this, value); // threadLocal.set(T)
Thread.currentThread().threadLocals.get(this); // threadLocal.get()

Thread

可以看到Thread类里有两个成员变量:threadLocals和inheritableThreadLocals;类型都是ThreadLocal.ThreadLocalMap。
在这里插入图片描述
那这两个变量的作用是什么呢?我们后续分析

ThreadLocal

我们先看ThreadLocal的类结构:
在这里插入图片描述

我们挑几个拿出来分析:

  • 类SuppliedThreadLocal:
  • 类ThreadLocalMap: 下节讲
  • nextHashCode()方法:
  • initialValue()方法:
  • get()方法:
  • set(T)方法:
  • createMap(Thread,T)方法:
  • threadLocalHashCode :
  • nextHashCode 静态变量:
  • HASH_INCREMENT 常量:

解释

ThreadLocal类顾名思义可以理解为线程本地变量。也就是说如果定义了一个ThreadLocal,每个线程往这个ThreadLocal中读写是线程隔离,互相之间不会影响的。它提供了一种将可变数据通过每个线程有自己的独立副本从而实现线程封闭的机制。

ThreadLocalMap

在这里插入图片描述

解释

Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。每个线程在往某个ThreadLocal里塞值的时候,都会往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。

存储结构

既然是个map(注意不要与java.util.map混为一谈,这里指的是概念上的map),当然得要有自己的key和value,我们可以将其简单视作key为ThreadLocal,value为实际放入的值。之所以说是简单视作,因为实际上ThreadLocal中存放的是ThreadLocal的弱引用。我们来看看ThreadLocalMap里的节点是如何定义的。

static class Entry extends WeakReference<java.lang.ThreadLocal<?>> {
    // 往ThreadLocal里实际塞入的值
    Object value;

    Entry(java.lang.ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

Entry便是ThreadLocalMap里定义的节点,它继承了WeakReference类,定义了一个类型为Object的value,用于存放塞到ThreadLocal里的值。

核心:由于ThreadLocalMap使用线性探测法来解决散列冲突,所以实际上Entry[]数组在程序逻辑上是作为一个环形存在的。
线性探测法:直接使用数组来存储数据。可以想象成一个停车问题。若当前车位已经有车,则你就继续往前开,直到找到下一个为空的车位。

实现步骤:
1,得到 key
2,计算得 hashValue
3,若不冲突,则直接填入数组
4,若冲突,则使 hashValue++ ,也就是往后找,直到找到第一个 data[hashValue] 为空的情况,则填入。若到了尾部可循环到前面。

如图:(虚线表示弱引用,实线表示强引用。)
在这里插入图片描述
ThreadLocalMap维护了Entry环形数组,数组中元素Entry的逻辑上的key为某个ThreadLocal对象(实际上是指向该ThreadLocal对象的弱引用),value为代码中该线程往该ThreadLoacl变量实际塞入的值。

扫描二维码关注公众号,回复: 5444769 查看本文章

弱引用

因为如果这里使用普通的key-value形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。弱引用是Java中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次GC。当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利。
注:如果hashmap中,map中的key变为null,由于key和value之间存在强引用,所以,value不会被回收

ThreadLocal实现中特别值得注意的是1点:每个线程中都保存着一个threadLocals实例,该实例是Map接口的实现 – ThreadLocalMap。而这个类实现的特殊地方在于,ThreadLocalMap中的Entry中的key类型是WeakReference而非ThreadLocal。为什么ThreadLocalMap中需要使用WeakReference作为key类型,那么首先需要理解WeakReference的意义。

WeakReference是Java语言规范中为了区别直接的对象引用(程序中通过构造函数声明出来的对象引用)而定义的另外一种引用关系。WeakReference标志性的特点是:reference实例不会影响到被应用对象的GC回收行为(即只要对象被除WeakReference对象之外所有的对象解除引用后,该对象便可以被GC回收),只不过在被对象回收之后,reference实例想获得被应用的对象时程序会返回null。

理解了WeakReference之后,ThreadLocalMap使用它的目的也相对清晰了:当threadLocal实例可以被GC回收时,系统可以检测到该threadLocal对应的Entry是否已经过期(根据reference.get() == null来判断,如果为true则表示过期,程序内部称为stale slots)来自动做一些清除工作,否则如果不清除的话容易产生内存无法释放的问题:value对应的对象即使不再使用,但由于被threadLocalMap所引用导致无法被GC回收。实际代码中,ThreadLocalMap会在set,get以及resize等方法中对stale slots做自动删除(set以及get不保证所有过期slots会在操作中会被删除,而resize则会删除threadLocalMap中所有的过期slots)。当然将threadLocal对象设置为null并不能完全避免内存泄露对象,最安全的办法仍然是调用ThreadLocal的remove方法,来彻底避免可能的内存泄露。

InheritableThreadLocal原理

参考资料

另 :weakHashMap相关:https://www.cnblogs.com/skywang12345/p/3311092.html

猜你喜欢

转载自blog.csdn.net/iverson2010112228/article/details/88235267