ThreadLocal源码阅读

ThreadLocal

一、概述

学习ThreadLocal的源码之前首先,我们要清楚ThreadLocal是干什么的,怎么使用。

作用:

threadlocal而是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据。也就是说在一个线程中new 一个ThreadLocal,这个ThreadLocal可以来存储数据,并且数据只能在使用它的线程中才能get()取出来。如果你想多存几个值怎么办那,那只好多创建几个了。

使用:

hreadLocal<String> threadLocal = new ThreadLocal<>();
                threadLocal.set("666");
                threadLocal.get();

使用方法也很简单就是创建出一个来set()、get()。就完事了。

二、原理

我们会思考里面就是是什么原理那,怎么实现的这种情况那,不妨我们大胆的猜测一下。会不会是这种情况,我们在ThreadLocal中有一个Map对象,每个线程会把自身当作key值去Map管理数据呐,我们来验证一下。

1、set()方法

public void set(T value) {
        Thread t = Thread.currentThread(); //获取当前线程
        ThreadLocalMap map = getMap(t); //实际存储的数据结构类型
        if (map != null)
            map.set(this, value);//如果存在map就直接set,没有则创建map并set
        else
            createMap(t, value);
    }

1、首先我们会得到当前的线程,得到当前的线程之后,我们会通过去得到一个map。我们来看看getMap()怎么执行的。

1.1、getMap()

 ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;//thread中维护了一个ThreadLocalMap
    }

直接返回了当前现成的threadLocals变量,我们看看这个变量是什么意思。

   ThreadLocal.ThreadLocalMap threadLocals = null; //thread的成员变量threadLocals,类型是ThreadLocal的内部类ThreadLocalMap

返回了一个ThreadLocal的内部类。我们大致看一下这个类的方法。

1.2、map.set(this, value);

 private void set(ThreadLocal<?> key, Object value) {

            Entry[] tab = table;
            int len = tab.length;
            //获取索引值,一个线程维护一个ThreadLocalMap,里面有一个初始化为16的数组,当有多个ThreadLocal时,一个线程在多个ThreadLocal中是同一个ThreadLocalMap,
            //通过不同的ThreadLocal做位运算获取索引值,得到相应value.
            int i = key.threadLocalHashCode & (len - 1);
			//注意这里结束循环的条件是e != null,这个很重要,还记得上面讲的开放地址法吗?忘记的回到上面看下,一定看懂才往下走,不然白白浪费时间
			//这里遍历的逻辑是,先通过hash 找到数组下标,然后寻找相等的ThreadLocal对象
			//找不到就往下一个index找,有两种可能会退出这个循环
			// 1.找到了相同ThreadLocal对象
			// 2.一直往数组下一个下标查询,直到下一个下标对应的是null 跳出
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {//当前遍历不到,后移
                ThreadLocal<?> k = e.get();//返回此位置的key

                if (k == key) {//如果相等
                    e.value = value;//则返回此索引对应的value
                    return;
                }
				// k==null&&e!=null 说明key被垃圾回收了,这里涉及到弱引用,接下来讲
                if (k == null) {
                    //被回收的话就需要替换掉过期过期的值,把新的值放在这里返回
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            //如果没找到就创建新值
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();//满足条件扩容两倍
        }

大体逻辑可以类比HasaMap();有以下不同:

  1. HashMap 的数据结构是数组+链表
  2. ThreadLocalMap的数据结构仅仅是数组
  3. HashMap 是通过链地址法解决hash 冲突的问题
  4. ThreadLocalMap 是通过开放地址法来解决hash 冲突的问题
  5. HashMap 里面的Entry 内部类的引用都是强引用
  6. ThreadLocalMap里面的Entry 内部类中的key 是弱引用,value 是强引用

我们看一下replaceStaleEntry(key, value, i);这个方法。

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

//这里采用的是从当前的staleSlot 位置向前面遍历,i--
//这样的话是为了把前面所有的的已经被垃圾回收的也一起释放空间出来
//(注意这里只是key 被回收,value还没被回收,entry更加没回收,所以需要让他们回收),
//同时也避免这样存在很多过期的对象的占用,导致这个时候刚好来了一个新的元素达到阀值而触发一次新的rehash
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                //slotToExpunge 记录staleSlot左手边第一个空的entry 到staleSlot 之间key过期最小的index
                if (e.get() == null)
                    slotToExpunge = i;
// 这个时候是从数组下标小的往下标大的方向遍历,i++,刚好跟上面相反。
//这两个遍历就是为了在左边遇到的第一个空的entry到右边遇到的第一空的 entry之间查询所有过期的对象。
//注意:在右边如果找到需要设置值的key(这个例子是key=15)相同的时候就开始清理,然后返回,不再继续遍历下去了
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
//说明之前已经存在相同的key,所以需要替换旧的值并且和前面那个过期的对象的进行交换位置,
//交换的目的下面会解释
                if (k == key) {
                    e.value = value;
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

//这里的意思就是前面的第一个for 循环(i--)往前查找的时候没有找到过期的,只有staleSlot
// 这个过期,由于前面过期的对象已经通过交换位置的方式放到index=i上了,
// 所以需要清理的位置是i,而不是传过来的staleSlot
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    //进行清理过期数据
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

// 如果我们在第一个for 循环(i--) 向前遍历的时候没有找到任何过期的对象
// 那么我们需要把slotToExpunge 设置为向后遍历(i++) 的第一个过期对象的位置
// 因为如果整个数组都没有找到要设置的key 的时候,该key 会设置在该staleSlot的位置上
//如果数组中存在要设置的key,那么上面也会通过交换位置的时候把有效值移到staleSlot位置上
//综上所述,staleSlot位置上不管怎么样,存放的都是有效的值,所以不需要清理的
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

         // 如果key 在数组中没有存在,那么直接新建一个新的放进去就可以
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

           // 如果有其他已经过期的对象,那么需要清理他
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

举个例子讲解以下这个方法。

第一个for 循环是向前遍历数据的,直到遍历到空的entry 就停止(这个是根据开放地址的线性探测法),这里的例子就是遍历到index=1就停止了。向前遍历的过程同时会找出过期的key,这个时候找到的是下标index=3 的为过期,进入到

if (e.get() == null)
	slotToExpunge = i;

注意此时slotToExpunge=3,staleSlot=5
第二个for 循环是从index=staleSlot开始,向后编列的,找出是否有和当前匹配的key,有的话进行清理过期的对象和重新设置当前的值。这个例子遍历到index=6 的时候,匹配到key=15的值,进入如下代码:

if (k == key) {
    e.value = value;
    tab[i] = tab[staleSlot];
    tab[staleSlot] = e;
    if (slotToExpunge == staleSlot)
        slotToExpunge = i;
    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    return;
}

先进行数据交换,注意此时slotToExpunge=3,staleSlot=5,i=6。这里就是把5 和6 的位置的元素进行交换,并且设置新的value=new,交换后的图是这样的

img

为什么要交换
这里解释下为什么交换,我们先来看看如果不交换的话,经过设置值和清理过期对象,会是以下这张图

img

这个时候如果我们再一次设置一个key=15,value=new2 的值,通过f(15)=5,这个时候由于上次index=5是过期对象,被清空了,所以可以存在数据,那么就直接存放在这里了,所以说根据开放性寻址原则,需要把寻址起始位置占满。

img

你看,这样整个数组就存在两个key=15 的数据了,这样是不允许的,所以一定要交换数据

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

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    //这里设置为null ,方便让GC 回收
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
//这里主要的作用是由于采用了开放地址法,所以删除的元素是多个冲突元素中的一个,需要对后面的元素作
//处理,可以简单理解就是让后面的元素往前面移动
//为什么要这样做呢?主要是开放地址寻找元素的时候,遇到null 就停止寻找了,你前面k==null
//的时候已经设置entry为null了,不移动的话,那么后面的元素就永远访问不了了,下面会画图进行解释说明
                    int h = k.threadLocalHashCode & (len - 1);
                    //他们不相等,说明是经过hash 是有冲突的
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

接下来我们详细模拟下整个过程 根据我们的例子,key=5,15,25 都是冲突的,并且k=5的值已经过期,经过replaceStaleEntry 方法,在进入expungeStaleEntry 方法之前,数据结构是这样的

img

此时传进来的参数staleSlot=6,

if (k == null) {
//这里设置为null ,方便让GC 回收
    e.value = null;
    tab[i] = null;
    size--;
}

这个时候会把index=6设置为null,数据结构变成下面的情况

img

接下来我们会遍历到i=7,经过int h = k.threadLocalHashCode & (len - 1) (实际上对应我们的举例的函数int h= f(25)); 得到的h=5,而25实际存放在index=7 的位置上,这个时候我们需要从h=5的位置上重新开始编列,直到遇到空的entry 为止 int h = k.threadLocalHashCode & (len - 1);

if (h != i) {
    tab[i] = null;
    while (tab[h] != null)
    	h = nextIndex(h, len);
    tab[h] = e;
}


这个时候h=6,并把k=25 的值移到index=6 的位置上,同时设置index=7 为空,如下图

img

其实目的跟replaceStaleEntry 交换位置的原理是一样的,为了防止由于回收掉中间那个冲突的值,导致后面冲突的值没办法找到(因为e==null 就跳出循环了)

ThreadLocal 内存溢出问题:
通过上面的分析,我们知道expungeStaleEntry() 方法是帮助垃圾回收的,根据源码,我们可以发现 get 和set 方法都可能触发清理方法expungeStaleEntry(),所以正常情况下是不会有内存溢出的 但是如果我们没有调用get 和set 的时候就会可能面临着内存溢出,养成好习惯不再使用的时候调用remove(),加快垃圾回收,避免内存溢出
退一步说,就算我们没有调用get 和set 和remove 方法,线程结束的时候,也就没有强引用再指向ThreadLocal 中的ThreadLocalMap了,这样ThreadLocalMap 和里面的元素也会被回收掉,但是有一种危险是,如果线程是线程池的, 在线程执行完代码的时候并没有结束,只是归还给线程池,这个时候ThreadLocalMap 和里面的元素是不会回收掉的

1.3、createMap(t, value);

void createMap(Thread t, T firstValue) {
        //实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

2、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();
    }

2.1、map.getEntry(this)

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

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

2.2、setInitialValue()

private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

就是放一个null进去。

3、Entry

就是静态类ThreadLocalMap的结点。

static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

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

3.1、super(k)

public WeakReference(T referent) {
        super(referent);
    }

再追

Reference(T referent) {
        this(referent, null);
    }

由此可见Entry是继承于弱引用的。

img

所以才会出现,key被回收但是value还在的局面。

整体结构


我们的猜测可能是错误的,它的应该是每个线程有一个Map,ThreadLocal作为key去存储对应的值。

注意点:

  • entry[]中的key是弱引用。
  • ThreadLocal本身并不存储值,他只是作为一个key取存储值。

内存泄漏造成原因:

1、当一个对象只剩弱引用,下一次垃圾回收就会被回收。

2、entry[]中的key是对堆中的ThreadLocal是弱引用

3、当我们手动把栈中的ThreadLocal引用置位空,堆中的ThreadLocal就没有强引用指向,就会被回收。

4、导致出现了key为null的entry[],且永远无法回收。

解决方法:主动调用remove方法。

参考:面试官连环炮轰炸的ThreadLocal 吃透源码的每一个细节和设计原理

posted @ 2021-01-10 17:51  红警贼秀  阅读(90)  评论(0编辑  收藏  举报