【关于ThreadLocal】你真的很了解ThreadLocal吗?

ThreadLocal的基本原理

ThreadLocal中有个静态内部类ThreadLocalMap,ThreadlocalMap里面包含一个Entry数组,Entry的定义如下

static class Entry extends WeakReference> {
    Object value;
    Entry(ThreadLocal k, Object v) {
        super(k);
        value = v;
    }
}

那ThreadLocal是如何保证每个线程内有一份单独的变量呢?原因在于,Thread类内部持有ThreadLocalMap的引用

ThreadLocal.ThreadLocalMap threadLocals = null;

【关于ThreadLocal】你真的很了解ThreadLocal吗?_第1张图片
接下来我们就来看ThreadLocal提供的几个关键方法

set方法

public void set(T value)

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

在执行set的时候,首先获取当前线程,然后获取线程内部的ThreadlocalMap对象,如果map为null,就执行createMap方法

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

接着看看ThreadLocalMap的构造方法

ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

首先创建一个Entry数组,长度为INITIAL_CAPACITY,然后我们对INITIAL_CAPACITY进行取余运算,确定出当前Entry在数组中的位置,接着令size为1,标识当前table的大小为1,最后设置了table进行扩容的大小,装在因子大小为 2/3 ,也就是说当前table的size如果大于或等于 2/3 * length,就进行扩容。

如果从Thread中获取的map不为null,就执行map.set(this, value)方法

 private void set(ThreadLocal key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

首先根据当前ThreadLocal的hashcode获取在table中的索引,将这个entry作为初始化参数,执行for循环,每次循环都会获取下一个entry,那么有个问题,为什么要执行for循环呢?一个ThreadLocal关联的value是确定的呀,直接通过ThreadLocal的hashcode就可以确定出其在table中的索引,然后将对应的entry取出来,不就好了吗?为什么这里要执行循环呢?原因在于可能存在hash冲突,而ThreadLocal解决hash冲突的办法是开放寻址法,也就是说产生了hash冲突就向后进行环形搜索空位,因此这里会执行for循环。

执行循环时,如果获取的entry不为null

  1. 如果entry的k等于当前的ThreadLocal引用,直接更新value的值
  2. 如果entry的k为null,说明这个entry脏了,就会去执行replaceStaleEntry方法
private void replaceStaleEntry(ThreadLocal key, Object value, int staleSlot) {
     Entry[] tab = table;
     int len = tab.length;
     Entry e;

     int slotToExpunge = staleSlot;
     for (int i = prevIndex(staleSlot, len);
          (e = tab[i]) != null;
          i = prevIndex(i, len))
         if (e.get() == null)
             slotToExpunge = i;
             
     for (int i = nextIndex(staleSlot, len);
          (e = tab[i]) != null;
          i = nextIndex(i, len)) {
         ThreadLocal k = e.get();

         if (k == key) {
             e.value = value;

             tab[i] = tab[staleSlot];
             tab[staleSlot] = e;

             if (slotToExpunge == staleSlot)
                 slotToExpunge = i;
             cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
             return;
         }

         if (k == null && slotToExpunge == staleSlot)
             slotToExpunge = i;
     }

     tab[staleSlot].value = null;
     tab[staleSlot] = new Entry(key, value);

     if (slotToExpunge != staleSlot)
         cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
 }

staleSlot是当前脏entry的索引,这个replaceStaleEntry方法做了哪些事呢。

首先向前搜索

  • 从staleSlot开始,向前搜索,找到第一个脏数据,直至遇到table[i]为null,那么前向搜索就结束,并把脏数据的索引保存在slotToExpunge中,如果前向搜索没有找到脏数据,slotToExpunge的值就为staleSlot

然后向后搜索

  1. 如果发现key相同的entry
  • 就更新的值填写在该entry中,并且将staleSlot所在的脏entry与当前的entry交换,这样交换的结果就是把更新后的entry填在了staleSlot的位置,因此通过ThreadLocal的hashcode就能直接索引到该位置

  • 如果在搜索的过程中,还没有发现其余脏的entry,那么就以当前位置保存在slotToExpunge,并以这个位置为起点,对脏entry执行清理工作。

  1. 如果发现key为null的entry
  • 并且向前的搜索没有找到脏entry,后面就以这个位置为起点,执行清理的工作

最后

如果在查找过程中 ,没有找到可以覆盖的entry,就以value构造一个新的entry,并插在staleSlot位置,如果还存在脏的entry,就执行清理工作。

执行循环时,如果获取的entry为null

那么就在当前null的位置构造出一个新的entry,然后可能需要进行rehash操作。

至此,set方法基本介绍完了,下面来看看get方法

get方法

public T get()

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

在执行get的时候,依旧首先获取当前的线程,然后获取线程内部的ThreadLocalMap对象,接着把当前的this传进去,即可获得与当前ThreadLocal相关联的变量。

我们看一下这个getEntry方法,首先通过ThreadLocal的hashcode索引到具体的table中的某个entry,如果找到的tntry不为null,并且tntry的k与当前的ThreadLocal引用相同,那么我们就返回对应的value。

private Entry getEntry(ThreadLocal key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

如果没找到符合条件的entry,就会去执行getEntryAfterMiss(key, i, e)方法。

private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    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;
}

getEntryAfterMiss

我们看if语句里面的几个判断条件,第一个是k == key,为什么会有k == key这样的判断条件呢?我们在getEntry方法中,就进行了判断,如果entry不为null,并且entry的k等于key,我们就返回这个entry,那么剩下的entry的k肯定不等于key呀,其实还是同一个原因,存在hash冲突的可能,ThreadLocal解决hash冲突采用的是开放寻址法,而不是Hashmap采用的拉链法,因此会向后环形搜索,如果k = null,就会去执行expungeStaleEntry(i)方法。

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

    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal k = e.get();
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

expungeStaleEntry

该方法主要做了四件事情

  1. 清理当前脏entry,即将其value引用置为null,那么这个value在下一次GC时就会被回收掉,同时也将table[staleSlot]为null后以便于存放新的entry,并将size-1。

  2. 从当前staleSlot位置向后环形(nextIndex)继续搜索,如果遇到哈希桶为null的时候退出;

  3. 若在搜索过程再次遇到脏entry,也同样将其继续清除。

  4. 如果threadLocal的hashcode定位的索引位置与当前threadLocal实际存放在table中的位置不一样,会执行rehash

remove方法

remove方法就不做介绍了,相信有了set和get方法的认知,renove方法还是比较好理解的

ThreadLocal引起的内存泄漏

对ThreadLocal有过了解的人,可能会这样觉得,ThreadLocal的内存泄漏是由ThreadLocalMap中的弱引用造成的,假设我们不使用弱引用,使用强引用,看看会发生什么情况。

使用强引用

假设ThreadLocal是强引用,示意图图下
【关于ThreadLocal】你真的很了解ThreadLocal吗?_第2张图片
如图所示,堆中的ThreadLocal有两个强引用指向着它,分别标号为1,2。
那么倘若我们想清除ThreadLocal,令ThreadLocalRef = null,那么当前的ThreadLocal对象也不会被清除,因为还有2这条强引用指向着它,糟糕的是,倘若当前的线程属于某个线程池,那么这个线程的生命周期将比较久,始终存在着引用链指向ThreadLocal,那么ThreadLocal这个对象就不会被回收,entry里对应的数据就白白占据着堆内存,得不到访问,也无法清除,除非我们结束当前的Thread。因此在这种情况下,也会存在内存泄漏,而且情况更加严重,还没有有效的办法清楚无用的数据。

使用弱引用

【关于ThreadLocal】你真的很了解ThreadLocal吗?_第3张图片
尽管存在内存泄漏的可能,但是ThreadLocal的生命周期里,不管是set方法,还是get方法,都对key为null的脏entry做了清理,尽可能地去避免内存泄漏的情况。而且,倘若我们想清除ThreadLocal,直接令ThreadLocalRef = null即可,这也是符合我们的编程习惯的,这个ThreadLocal由于没有引用链指向,因此会被GC回收的,Thread内部的entry数组中还存在着与这个ThreadLocal相关联的数据,因此在清除ThreadLocal前,我们要调用remove方法,养成良好的编程习惯。

你可能感兴趣的:(并发编程,thread,内存泄漏,并发编程,java)