来谈谈本地变量 java.lang.ThreadLocal 和 java.lang.InheritableThreadLocal(JDK1.8 源码分析)

本地变量源码刨析


前言

ThreadLocal / InheritableThreadLocal / Thread 之间关系
在这里插入图片描述

1.我们都知道共享变量是可以被所有线程访问大的变量,因此访问这些变量的时候我们可能需要对其进行同步以防止多个线程访问时而导致的数据不一致的情况。而本地变量则时线程所私有的,一般情况下一个线程是无法直接去访问到另一个线程的私有变量的,所有无需考虑同步问题。
2.ThreadLocal是为了保存线程本地变量而设计的一个类。当我们通过他的set方法设置一个变量的时候实际上是设置在该线程本地。


提示:以下是本篇文章正文内容,下面案例可供参考

一、ThreadLocal 源码部分

1.构造器

    public ThreadLocal() {
    }

2.成员方法

private final int threadLocalHashCode = nextHashCode();

private static AtomicInteger nextHashCode = new AtomicInteger();

private static final int HASH_INCREMENT = 0x61c88647;

3.主要方法

public void set(T value) {
    Thread t = Thread.currentThread();
    //ThreadLocalMap 是ThreadLocal的静态内部类 是一个map结构实际存数据的地方
    ThreadLocalMap map = getMap(t); 
    if (map != null)
        map.set(this, value); //map存在将数据放入map中去
    else
        createMap(t, value);//map不存在直接创建
}

可以看到一开始,是获取当前的当前线程,然后获取该线程的Map数据结构,最后通过判断该map在线程中是否被创建,若创建则直接放入其中,否则创建一个Map放入其中。

ThreadLocal中getMap是获得本线程Thread实例中的threadLocals变量。可见Thread中存在一个实例变量threadLocals,而该变量类型是ThreadLocalMap(ThreadLocal的一个静态内部类是一个以ThreadLocal实例作为键,Object作为值的Map结构)。所以我们说ThreadLocal实际上是一个工具外壳,因为实际的数据都是存在Thread实例变量threadLocal中的,而他只是引用了该变量。这里我们会问为什么会使用Map的数据结构来存线程本地数据呢?我们看👇图。

在这里插入图片描述
可以看到我们线程可能不止要存一个ThreadLocal对象,而且每个对象存的值也可以是不想同的(value的类型是由泛型T决定的),并且每个不同的ThreadLocal对象对应一个value值。(因此我们线程可以 创建多个ThreadLocal来对应不同的值作为本地变量)

我们先来看看ThreadLocalMap的创建。显然只有当我们线程第一次set向他的threadLocal放东西的时候才会调用该方法创建一个ThreadLocalMap对象。

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);//给当前线程变量赋值
}

//ThreadLocal.ThreadLocalMap
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];//创建一个数组
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //hash计算index
    table[i] = new Entry(firstKey, firstValue); //创建新元素
    size = 1; //当前元素个数
    setThreshold(INITIAL_CAPACITY); //设置阈值(超过阈值进行扩容)
}
/*该构造方法先为table分配空间(于HashMap中table差不多,但是没有HashMap中的链表,
  红黑树等数据结构,一个位置只放一个key-value,当然如果发生hash碰撞元素位置会向后挪
  直到找到一个何时的位置)。获取第一个key的hash值,存入table中,然后设置大小和阈值。*/

我们再来看threadLocal被初始化后再向其中加入数据调用的set方法(ThreadLocalMap的set方法)

private void set(ThreadLocal<?> key, Object value) {
    
    Entry[] tab = table;   //将桶引用赋值给临时变量
    int len = tab.length;  //获得桶长度
    int i = key.threadLocalHashCode & (len-1);//新值在桶中的位置
    
/* 这里是环形循环,当遇到hash碰撞的时候则数组的位置向后移动,直到遇到一个空位或者遇到key相同的位置替换掉他的value后返回*/
    for (Entry e = tab[i];
         e != null;//遇到空位直接跳出
         e = tab[i = nextIndex(i, len)]) { //nextIndex(i, len) : return ((i + 1 < len) ? i + 1 : 0);
        ThreadLocal<?> k = e.get();

        //如果新值key是相同的引用替换他的value然后返回
        if (k == key) { 
            e.value = value;
            return;
        }
        /*在循环遍历的时候也会进行脏数据(k为null)的数据清除*/
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }

/*不是以上两种情况nextIndex跳到桶中下一个位置(如果已经到最后一个位置,又会回到0位置处*/
    }

    tab[i] = new Entry(key, value);//将空位放入新值
    int sz = ++size;//size大小加1
    //清理脏数据 然后判断是否超过阈值
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();  //重新调整元素位置
}

我们先来分析这个cleanSomeSlots(清理一些脏数据)方法

private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;  //删除标志(最后返回该值表示是否清理了脏数据)
    Entry[] tab = table;
    int len = tab.length;

    do { 
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) { //循环过程中遇到脏数据需要加大搜索范围
            n = len;  //n被重置循环又将进行log2(n)次
            removed = true;  //删除标志为真
            i = expungeStaleEntry(i);  //删除i位置的脏数据,i被置为脏数据之后的第一个null元素
        }
    } while ( (n >>>= 1) != 0); //这里循环log2(n)次(考虑了时间因素:搜索范围较小)
    return removed;  //清理了脏数据返回true
}

可以看到cleanSomeSlots方法是清理位置i之后的脏数据,但是他直会遍历log2(n),这里考虑到了时间因素,但是到我们遇到脏数据时(说明之后可能还有很多脏数据要清理)我们会增加遍历的次数又将遍历log2(n)次,这是一种向前试探的方法。最后如果我们只要遇到一个脏数据并删除了方法会返回remove = true

方法expungeStaleEntry(清除脏数据)解析

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    //直接对脏数据位置赋值为null之后GC会回收
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    Entry e;
    int i;

    for (i = nextIndex(staleSlot, len);//从脏数据位置下一个位置开始重新hash直到遇到null
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {    //遍历过程中遇到脏数据直接将器置为null 
            e.value = null;
            tab[i] = null;
            size--;
        } else { //否则重新hash(将元素的位置换到离hash值取余后位置的最近的后面
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {  //如果已经位于该位置上不会进入if语句
                tab[i] = null;  //将当前位置置为空
                //直到遇到最近的位置后放入e
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;  //放回脏数据之后(没有进行rehash)第一个为null的元素的位置
}

从这个函数我们可以看的出来我们传入一个脏数据的下标,我们不仅将该位置清空,而且还会向后进行遍历(rehash)。为什么我们要rehash呢?我举个例子,当我们三个元素发生hash碰撞的时候,他们的位置分别为1 2 3 此时我们第二个位置变为脏数据了(也就是key为null)并且被清除了(该位置元素为null),当我们通过get方法(之后会分析get方法)获取第3个元素时,我们首先会定位到1这个位置,然后向后遍历直到遇到key相同的元素返回该元素的value或者遇到元素为null的元素就返回null。显然我们到2时就返回了。所以我们清除一个元素的时候要进行rehash使得元素真实的位置与hash得到的位置之间不能存在null元素,所以我们在调用expungeStaleEntry方法删除第2个元素的时候也会对之后的元素进行rehash比如将3位置的元素交换到2位置当然在这过程中遇到脏数据也会将其清除。这要做保证了发生hash碰撞的元素之间不会存在null值。

我们看以下这张图来看上面两个方法的工作过程
在这里插入图片描述
我们再来看看replaceStaleEntry(取代脏数据)方法(前面set方法遇到脏数据的时候会调用该方法)staleSlot:脏数据的位置

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table; 
    int len = tab.length; 
    Entry e;

    int slotToExpunge = staleSlot; //保存脏数据下标 

    //以脏数据下标为基准向前循环遍历遇到第一个为null的元素停止
    //该循环主要用于查询当前脏数据下标之前的最远的一个脏数据
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null) //如果遍历过程中遇到脏数据,记录该下标
            slotToExpunge = i;

    //以脏数据下标为基准向后遍历遇到第一个为null的元素停止
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        
        if (k == key) {//遍历时遇到key相同的引用
            //交换该位置与脏数据的位置
            e.value = value;
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            /*1如果开始在向前搜索过程中没有遇到脏数据。先清理当前位置脏数据,然后,以当前位置第一个为null的元素之后的脏数据进行log2(n)次循环清理,具体过程参考前面的cleanSomeSlots expungeStaleEntry 方法
2如果向前搜索过程中遇到脏数据,清理遇到的脏数据之后操作与上面一样*/
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return; //直接返回
        }
        
        //向后遍历过程中遇到脏数据,并且开始在向前遍历过程中没有遇到脏数据
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i; //更新遇到脏数据的位置
    }

    // 如果没有找到相同的key,将脏数据value设为null
    tab[staleSlot].value = null;
    //将脏数据位置插入新的我们要加入的值
    tab[staleSlot] = new Entry(key, value);

    // 将之前的记录的脏数据进行相应的清理
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

这个方法作用是将给定元素取代脏数据的位置的元素,但是他并不是直接就取代了而是先通过循环向后遍历先看是否有与给定元素key相同的的元素,有的话会先将其取代然后置换到脏数据的位置上。当然在这些过程中也进行了很多其他的清除脏数据的操作。这里我分析的时候也有一些困惑,就是为什么一开始他要向前遍历找到第一个null元素的位置记录在slotToExpunge中(这个变量会用到之后清理脏数据时的开始位置),后来我上网看了一些博客查阅一些资料才知道,这里向前搜索的本意是因为当前位置为脏数据的位置,所以他认为他的周围(前面或者后面)也很大可能存在脏数据,所以他向前找一找看是否有脏数据,有的话就记录下来便于后面的清理。

在这里插入图片描述

到这里我么你大致对ThreadLocalMap的存值有了大致的了解。接下来我们看看怎么从中取值。

我们先来看看ThreadLocal的get方法

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t); //获取当前线程的ThreadLocalMap
    if (map != null) {  //map曾经被创建过
        ThreadLocalMap.Entry e = map.getEntry(this);//当前线程的threadLocal获取数组元素
        if (e != null) {  //数组元素不为空直接放回他的值
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();  //map为空初始化一个
}

以下为ThreadLocalMap的getEntry方法,用与通过ThreadLocal变量获取数组元素。

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);  //通过hash值定位元素位置
    Entry e = table[i];  
    if (e != null && e.get() == key) //如果当前位置的key与我们要找的key相同
        return e;               //直接返回该元素 
    else 
        return getEntryAfterMiss(key, i, e); //该位置key不同可能存在hash碰撞
}

我们再看看getEntryAfterMiss方法

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    
//从给定位置先后遍历直到遇到第一个null时
    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)  //找到我们预期的元素直接返回
            return e;
        if (k == null)  //遇到脏数据进行清除
            expungeStaleEntry(i);  //对脏数据进行清理
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

可见该方法是在首次hash定位没有到预期调用于是调用该方法从该位置之后进行遍历查询直到遇到null的元素若没有找到说明确实不存在返回null。当然在遍历过程中也在进行相关脏数据的清除。

我们接下里看下当get方法中map为null或者map为空没有找到相关元素调用的方法setInitialValue

private T setInitialValue() {
    T value = initialValue();  //返回null
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
/*判断当前的线程的map对象是否为空 为空新建一个map 不为空设置当前值在map中(value为null)*/
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

ThreadLocal的取值过程大概是这样,先通过key = ThreadLocal 判断key是否在map中。当然map如果没有创建则新建一个在将key设置在map里面其中value为null,如果map 存在且没找到对应的key则将key设置进map中其中value为null,找到则将其返回找到的value值。

最后我们来看看ThreadLocal中的remove方法,删除变量,上面的理解了这里就比较简单了。

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)  //map不为null 调用remove(this),为空不做任何事情
        m.remove(this);
}

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);  //先通过hash值定位到桶元素位置
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {  //找到了key
            e.clear();    //将该key置为空
            expungeStaleEntry(i);  //清除脏数据
            return;
        }
    }
}

在该方法中,先通过hash定位到key在桶中位置,然后向后遍历找key相同引用,找到则先通过clear()将其置为脏数据然后调用清理脏数据的方法清理。

总结:TreadLocal类为了防止脏数据的存在导致系统的内存泄露,在set,get,remove方法时总是在进行脏数据的查询以及清理,考虑到时间效率问题,他不会对整个散列数组进行完整的清理脏数据,而是通过在给定小范围内进行排查,如果有脏数据则可能扩大排查范围比如cleanSomeSlots方法。

二.分析InheritableThreadLocal类

InheritableThreadLocal类
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    protected T childValue(T parentValue) {
        return parentValue;
    }

    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

可以看到InheritableThreadLocal继承了ThreadLocal类并重写了3个方法。
我们来看createMap,之前是将创建的ThreadLocalMap对象赋值给线程的threadLocal对象这里将其给了线程的inheritableThreadLocals,可见实际存数据的位置变成了线程的另外一个变量inheritableThreadLocals。

我们先来看看一个程序
在这里插入图片描述
可以看到父线程设置的变量被子线程访问到了。正常情况下线程是无法访问到其他线程的局部变量的,但是这里用的InheritableThreadLocal类。当他创建子线程且他的InheritableThreadLocal变量不为null,将会将父线程之前设置在InheritableThreadLocal的值全部备份一份给子线程。

线程构造时调用的init方法中有这么一段代码其中inheritThreadLocals默认时true

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

意思就是父线程的inheritableThreadLocals不为null将会将其中的内容复制给他创建的子线程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值