ThreadLocal源码

Posted 顶风少年的博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ThreadLocal源码相关的知识,希望对你有一定的参考价值。

Thread和ThreadLocal的关系

初始化ThreadLocalMap和弱引用Entry
set方法与哈希冲突
清理槽
get方法也会清理槽
扩容
手动清理的重要性

Thread和ThreadLocal的关系

每个Thread中都持有一个ThreadLocalMap的实例,ThreadLocalMap是ThreadLocal的内部类。当Thread中没有ThreadLocalMap则需要先实例化ThreadLocalMap.

public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;//该对象是ThreadLocal中的内部类ThreadLocalMap
}

public class ThreadLocal<T> {
    //计算出来的hash值用它来确定Entry存放到哪个哈希槽
    private final int threadLocalHashCode = nextHashCode();
    //这是个固定值
    private static final int HASH_INCREMENT = 0x61c88647;
    //这个默认值是0,但new ThreadLocal后断点看到的值不是0,这是因为这是一个静态成员,在我们自己创建ThreadLocal前,main方法会先加载ThreadLocal给这个赋值了。
    private static AtomicInteger nextHashCode = new AtomicInteger();
    //每次调用该方法都会在原有的nextHashCode值上加上0x61c88647
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    
    //设置值
    public void set(T value) {
        Thread t = Thread.currentThread();//获取当前线程。
        ThreadLocalMap map = getMap(t);//获取当前线程的成员变量ThreadLocal.ThreadLocalMap threadLocals 
        if (map != null)
            map.set(this, value);//如果当前线程中的ThreadLocalMap已经实例化则set
        else
            createMap(t, value);//如果当前线程中的ThreadLocalMap没有实例化则实例化。
    }
    
    //在这走实例化ThreadLocalMap
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
}

初始化ThreadLocalMap和弱引用Entry

ThreadLocalMap里最重要的属性是Entry[],这个数组的初始长度是16,扩容阈值是size*2/3,Entry是ThreadLocalMap的内部类,Entry继承了弱引用。Entry里的key是ThreadLocal,value是设置的值。如果ThreadLocal栈引用结束了,在发生GC时虽然Entry还持有ThreadLocal的引用,这个ThreadLocal也会被垃圾回收,所以ThreadLocalMap常常伴随着扩容,清理操作。

static class ThreadLocalMap {
    //继承WeakReference很重要,WeakReferences是弱引用,在每次GC后都会回收弱引用对象里的引用值(若通过可达性分析查到引用值没有其他可达的Root,则会回收)
    //这个Entry就构成了唯一的key,也就是ThreadLocal。value是ThreadLocal.set(parameter)的参数
    static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);//最终传递给了Reference中的referent
                value = v;
            }
    }
    
    //ThreadLocalMap中的容器,一个线程持有一个ThreadLocalMap就相当于持有了一个Entry数组
    private Entry[] table;
    
    //数组的初始容量
    private static final int INITIAL_CAPACITY = 16;
    
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];//实例化数组
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//确定数组的位置
            //初始化ThreadLocalMap不会出现hash冲突。
            table[i] = new Entry(firstKey, firstValue);
            //已有元素++
            size = 1;
            //计算扩容阈值
            setThreshold(INITIAL_CAPACITY);
    }
    
    private void setThreshold(int len) {
            //初始化的容量第一次扩容的阈值是10,也就是说在数组的size是10的情况下就会触发扩容。
            threshold = len * 2 / 3;
    }
  }
}

set方法与哈希冲突

ThreadLocal的set方法是使用ThreadLocalMap的set方法。他分为四种情况。1 计算哈希后确定的槽内是null没有Entry表示没有哈希冲突,此时new一个Entry放入槽内。 2 计算哈希后确定的槽内有Entry但是槽内的Entry的key和当前的ThreadLocal相同则直接替换value。

3 计算哈希后确定的槽内有Entry但是key和当前ThreadLocal并不是同一个,则表示哈希冲突,此时顺着数组往右寻找,直到碰到有Entry但是没有key的槽,这表示这个槽内曾经有过ThreadLocal但是被GC掉了,此时这个槽是个废槽,可以替换掉Entry。 4 哈希冲突后向右

并没有找到被GC的槽,此时只能是找到距离最近的一个槽内没有Entry的,创建一个Entry存入。

static class ThreadLocalMap {
  //顺着当前下标往后查询。如果查询到了数组末尾则返回0号下标
    private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
    }
    
    //顺着当前下标往前查询。如果已经是0则返回数组末尾下标
    private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
    }
    
    private void set(ThreadLocal<?> key, Object value) {
            //拿到数组
            Entry[] tab = table;
            //数组长度
            int len = tab.length;
            //hash&length-1 效果类似hash%length
            int i = key.threadLocalHashCode & (len-1);
            //在这就要处理hash冲突了。如果hash值不冲突,那么算出来的index位置的Entry肯定是null.那么不会进入循环。
            //如果进入了循环,有没有可能两个if都不满足,有可能。这表示hash值冲突了,但是不是同一个ThreadLocal,并且hash值相同的槽内的ThreadLocal没有被GC。
            //那么只能是一直找到Entry是null的位置,然后跳出循环。
            for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                //如果是第一次循环到这里进去了,表示是同一个ThreadLocal多次设置值。则直接替换值。情况2
                if (k == key) {
                    e.value = value;
                    return;
                }
                //如果ThreadLocal为null则表示发生了GC把弱引用ThreadLocal清理了。
                //需要将当前set的key和value放入这个废掉的槽内,并且看看有没有需要清理的槽。情况3
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //没有进入循环,或者从循环跳出了。如果没有进入循环则i就是hash&length-1的位置表示当前算出来的hash值没有冲突,也是第一次使用。情况1
            //如果是循环跳出来的,则这个i就是hash&length-1.算出来的位置向后移动循环次数的位置。表示hash冲突了,并且冲突后的槽往后也都没有被GC
            //只能是往后顺延找别的可用槽。总之会找到一个在数组内Entry为空的位置。创建Entry放进数组。情况4
            tab[i] = new Entry(key, value);
            //已有元素++
            int sz = ++size;
       //如果没有清理槽,并且当前长度已经大于等于了阈值则扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } //走到这个方法表示通过hash&length-1的位置上的Entry中的key是null或者是哈希冲突后,往数组后查询发现有Entry中的key是null private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) { //数组 Entry[] tab = table; //数组长度 int len = tab.length; Entry e; //Entry为null的哈希槽 int slotToExpunge = staleSlot; //从Entry为null的哈希槽位置向前找,一直找到Entry为null停止 for (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i, len)){ //在向前寻找的过程中标记Entry中key为null的下标 if (e.get() == null) slotToExpunge = i; } //从Entry为null的哈希槽位置向后找,一直找到Entry为null停止 for (int i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) { //循环中的Entry中的key ThreadLocal<?> k = e.get(); if (k == key) { //如果key相同则替换value e.value = value; //将Entry中ThreadLocal为null的赋值给当前槽中 tab[i] = tab[staleSlot]; //在将Entry赋值给原来ThreadLocal为null的槽中。 //这两行操作相当于把槽里的内容互换了,达到的效果是前边的槽中的Entry有key,循环中的也就是后边的没有key tab[staleSlot] = e; //如果列表向左查询没有发现Entry中key有null的。则将当前循环中的槽的位置赋值。 //因为上两步操作已经把当前槽变成了key为null的槽,所以此处记录的位置就是key是null的位置 //如果向左查询有Entry里是null值那就表示这个区间内还有更左边有key是null的 if (slotToExpunge == staleSlot){ slotToExpunge = i; } //清理槽 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } //当前循环中的槽也是被GC过的。并且向左查询没有发现Entry为null的,就记录当前槽的位置。 if (k == null && slotToExpunge == staleSlot){ slotToExpunge = i; } } //出循环只有一种情况,key为null的Entry下标往后寻找没有发现与当前ThreadLocal相同的key。 //此时需要将原来Entry的value职位null。此操作用来释放内存。 tab[staleSlot].value = null; //创建一个新的Entry其中key是当前ThreadLocal,value是set的参数。将它放到被GC的位置。 tab[staleSlot] = new Entry(key, value); //如果向左查询有Entry中key是null的slotToExpunge就是在左边确定的 //如果向左查询没有Entry中key是null的,而向右查询有Entry中key是null的slotToExpunge就是右边确定的。 //如果两边都没有的情况表示当前区间内只有staleSlot一个为Entry是null的而这种情况下直接重新覆盖了Entry。不需要清理。条件不成立。 if (slotToExpunge != staleSlot){ cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); } } }

清理槽

expungeStaleEntry方法就是将废槽清空,然后将哈希冲突的槽重新分配位置,因为哈希冲突后是从哈希位向后移动寻找Entry是null的槽放入的,此后这些冲突的槽可能有被清理的,所以重新分配位置,方法的返回值是Entry为null的位置,cleanSomeSlots方法从这个位置

继续寻找有没有废槽,如果有就清理。

static class ThreadLocalMap {
      //接收的参数是槽里没有Entry的槽和当前数组的长度
    private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                //找到下一个槽的位置
                i = nextIndex(i, len);
                //获取槽内的Entry
                Entry e = tab[i];
                //如果槽内有Entry,并且Entry的key是null,表示这是个废槽。
                if (e != null && e.get() == null) {
                    n = len;
                    //有废槽肯定要清理的。
                    removed = true;
                    //方法返回下一个槽内没有Entry的槽下标
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);//这个操作相当于折半除2的操作。10,5,2,0,
            return removed;
    }
    
    //接收的参数是槽下标内有Entry,但是Entry的key被GC了。
    private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            //将槽清空
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            //Entry[]--
            size--;
            Entry e;
            int i;
            //循环的开始是废槽的下一个,终止条件是下一个槽有Entry
            for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {
                //拿到槽内的ThreadLocal
                ThreadLocal<?> k = e.get();
                //如果槽内的key也是null则表示这也是个废槽,则也需要做清空操作。
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    //如果槽内有的Entry有key,则通过hash值算出槽的位置。
                    int h = k.threadLocalHashCode & (len - 1);
                    //如果算出的槽位置不是当前的位置则表示这个key曾经哈希冲突了,所以位置并不是哈希位。
                    if (h != i) {
                        //将这个槽清空
                        tab[i] = null;
                        //从计算的哈希位开始循环,找到Entry为null的槽,将刚刚清空槽里的Entry重新安置。
                        while (tab[h] != null){
                            h = nextIndex(h, len);
                        }
                        //这一步的操作的意义在于,如果循环中有if条件满足的,这代表当前i这个位置之前有可用的槽,那就从哈希位开始往后找,找到空槽,重新安置这个Entry。
                        tab[h] = e;
                    }
                }
            }
            return i;//入参staleSlot是一个废槽,返回的i则是一个Entry为null的槽。
    }      
}

get方法也会清理槽

get方法通过当前ThreadLocal获取Entry[]中对应的Entry,如果ThreadLocalMap未实例化则实例化并返回null,通过哈希位找到了就返回,哈希位上的不是当前ThreadLocal则表示哈希冲突,继续在数组后寻找,如果途中发现有废槽则清理,如果最终没有找到则返回null。

public class ThreadLocal<T> {
       public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取线程内的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //如果ThreadLocalMap已经实例化
        if (map != null) {
            //通过ThreadLocal这个key到数组中找到Entry,是有可能找不到返回null的
            ThreadLocalMap.Entry e = map.getEntry(this);
            //如果找到了,返回Entry中的value
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //走到这里两种情况,1 ThreadLocalMap没有实例化,则实例化 2 从Entry[]没有找到对应ThreadLocal的Entry
        return setInitialValue();
    }
    
    //这个方法和set差不多,但是它可以返回null。
    private T setInitialValue() {
        //如果现在使用的就是ThreadLocal则一定返回null.
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
    //这个方法只能子类重写,意味着可以给ThreadLocal赋默认值。
    protected T initialValue() {
        return null;
    }  
}

static class ThreadLocalMap {
    static class ThreadLocalMap {

    //通过ThreadLocal找Entry
     private Entry getEntry(ThreadLocal<?> key) {
            //计算哈希位
            int i = key.threadLocalHashCode & (table.length - 1);
            //查看哈希位上的Entry
            Entry e = table[i];
            //如果Entry不是null或者Entry的key就是当前的ThreadLocal则找到了返回Entry
            if (e != null && e.get() == key){
                return e;
            }
            else{
                //如果从哈希位没有找到Entry或者Entry中的key不是当前ThreadLocal
                return getEntryAfterMiss(key, i, e);
            }
    }
    //接收的参数是当前ThreadLocal,计算的哈希位,和这个哈希位上的Entry
    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
            //从哈希位上开始循环寻找
            while (e != null) {
                ThreadLocal<?> k = e.get();
                //如果找到了key相同的则返回
                if (k == key){
                    return e;
                }    
                //如果当前槽内的key是null则要被清理
                if (k == null){
                    expungeStaleEntry(i);
                }else{
                    //如果槽内的key有值则继续寻找。直到Entry位null停止。
                    i = nextIndex(i, len);
                }
                //下一个位置继续找
                e = tab[i];
            }
            //如果循环结束了,表示哈希位往后寻找的key都不是当前的ThreadLocal,返回null。
            return null;
    }    
}

扩容

当数组内的元素到达阈值后触发扩容,扩容操作进行前会遍历数组进行清理。如果清理后仍然达到阈值则二倍扩容,循环扩容前的数组,根据新数组的长度重新计算哈希值,如果哈希槽内没有元素则放入,如果有则线性查询可用槽放入。然后用新的数组替换老的数组。

 

static class ThreadLocalMap {
   //扩容
         private void rehash() {
            //清理一遍槽
            expungeStaleEntries();
            //大于阈值扩容
            if (size >= threshold - threshold / 4)
                resize();
        }
        
        //全部清理
        private void expungeStaleEntries() {
            Entry[] tab = table;
            int len = tab.length;
            //遍历数组清理
            for (int j = 0; j < len; j++) {
                Entry e = tab[j];
                //发现废槽就清理
                if (e != null && e.get() == null){
                    expungeStaleEntry(j);
                }
            }
        }
        
        private void resize() {
            //扩容前的数组
            Entry[] oldTab = table;
            //扩容前数组的长度
            int oldLen = oldTab.length;
            //二倍扩容
            int newLen = oldLen * 2;
            //创建新的数组
            Entry[] newTab = new Entry[newLen];
            int count = 0;
            //遍历扩容前的数组
            for (int j = 0; j < oldLen; ++j) {
                Entry e = oldTab[j];
                //如果Entry不是null
                if (e != null) {
                    //获取key
                    ThreadLocal<?> k = e.get();
                    if (k == null) {
                        //key是null清理
                        e.value = null; 
                    } else {
                        //根据哈希值算出来在新的数组中的位置。
                        int h = k.threadLocalHashCode & (newLen - 1);
                        //新的位置上有Entry表示哈希冲突,则继续向后寻找。
                        while (newTab[h] != null){
                            h = nextIndex(h, newLen);
                        }
                        //找到一个Entry为null的位置存放Entry。
                        newTab[h] = e;
                        count++;
                    }
                }
            }
            //设置新的阈值
            setThreshold(newLen);
            //新数组内元素的总个数
            size = count;
            //替换数组
            table = newTab;
        }    
}

手动清理的重要性

clear方法就是把ThreadLocal从Entry中删除,然后删除Entry。这样Entry就没有了引用会被GC。如果不使用clear,那么就算是ThreadLocal栈内存释放了,这个对象还是存在于Thread里的ThreadLocalMap里的Entry[]数组中,除非遇到GC否则永远存在。手动清理的作用就在于不用等待GC自己把Entry清理。

public class ThreadLocal<T> {
  //通过ThreadLocalMap的remove方法释放内存
    public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
    }
    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }  
}

static class ThreadLocalMap {
   //通过当前ThreadLocal删除
    private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            //计算哈希位
            int i = key.threadLocalHashCode & (len-1);
            //循环找匹配的key
            for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    //调用Refereence的clear把key清空
                    e.clear();
                    //再次清理槽。
                    expungeStaleEntry(i);
                    return;
                }
            }
    }
}

public abstract class Reference<T> {
  private T referent;//这个就是ThreadLocal对象
    public void clear() {
        this.referent = null;
    }  
}

 

 

 

以上是关于ThreadLocal源码的主要内容,如果未能解决你的问题,请参考以下文章

ThreadLocal源码分析

ThreadLocal源码分析

ThreadLocal源码分析理解弱引用和内存泄漏

ThreadLocal源码分析理解弱引用和内存泄漏

ThreadLocal 源码分析

ThreadLocal源码分析理解弱引用和内存泄漏