Java多线程与高并发七(ThreadLocal源码)

ThreadLocal是什么

ThreadLocal提供了线程局部变量,由该类保存的变量,会分开线程,不同的线程会保存不同的变量副本。

import java.util.HashMap;
import java.util.Map;

public class ThreadLocalTest {
    ThreadLocal threadLocal = new ThreadLocal();
    Map<String, String> map = new HashMap<>(16);
    public static final String KEY = "key";
    String s1 = "dog";
    String s2 = "cat";

    public void f1() {
        threadLocal.set(s1);
        System.out.println("f1通过threadLocal获得的字符串是:" + threadLocal.get());
        map.put(KEY, s1);
        System.out.println("f1通过map获得的字符串是:" + threadLocal.get());
    }

    public void f2() {
        System.out.println("f2通过threadLocal获得的字符串是:" + threadLocal.get());
        System.out.println("f2通过map获得的字符串是:" + map.get(KEY));
        threadLocal.set(s2);
        map.put(KEY, s2);
        System.out.println("f2通过threadLocal获得的字符串是:" + threadLocal.get());
        System.out.println("f2通过map获得的字符串是:" + map.get(KEY));
    }

    public static void main(String[] args) throws Exception {

        ThreadLocalTest tlt = new ThreadLocalTest();
        new Thread(tlt::f1).start();
        Thread.sleep(2000);
        System.out.println("-----------------------------------------");
        new Thread(tlt::f2).start();

    }
}

示例代码很简单,测试了ThreadLocal与map的区别,可以看到结果显示如下:

f1通过threadLocal获得的字符串是:dog
f1通过map获得的字符串是:dog
-----------------------------------------
f2通过threadLocal获得的字符串是:null
f2通过map获得的字符串是:dog
f2通过threadLocal获得的字符串是:cat
f2通过map获得的字符串是:cat

Process finished with exit code 0

结果意味着,ThreadLocal存储的变量是线程独占的,f1方法开始的线程设置的threadLocal变量s1,在f2方法开始的线程中并拿不到。

那我们知道了ThreadLocal保存的变量实际上是线程隔离的。

ThreadLocal之get()方法

我们先从简单的get方法看起,看看ThreadLocal是如何实现线程隔离来获取设置的变量的。

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

可以看到原码中获取了当前线程t,又从当前线程中获取到了一个map,有点看不懂啊,这个ThreadLocalMap什么鬼?

我们看看ThreadLocal结构:

可以看到,ThreadLocalMap是ThreadLocal的内部类,而getMap方法呢?

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

很简单,就是从当前线程中拿到一个对象threadLocals,这个对象来自ThreadLocal的内部类ThreadLocalMap

如果map不为空,又去拿map中的entry,如果entry不为空,就把entry的值返回,整个获取过程就在最理想的情况下完成了。

Entry是ThreadLocalMap的内部类,这个类只有一个属性值,还继承了弱引用(作用后面再谈)。

我们看看map.getEntry方法干了些什么事?

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

这里会传入ThreadLocal对象,然后把threadLocal的hash码与(table.length-1)做与运算,拿到Entry数组的下标,这里值得一提的是为啥table的注释会那样写:

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

说这个table的长度必须是2的次方,因为要服务于取下标的【与运算】(&),其运算规则是:

         

运算规则:0 & 0 = 0; 0 & 1 = 0; 1 & 0 = 0; 1 & 1 = 1;

如果table的长度是2的次方,那么table.length-1的二进制码就会是:1111···,比如:

table.length-1二进制码
2 ^ 3-1    (其值为7)111
2 ^ 4-1    (其值为15)1111
2 ^ 5-1    (其值为31)11111

做与运算的时候,结果才会均匀分布。因为如果二进制码是0,不论与什么(0或1)运算都是0,其结果唯一,不具有均匀分布特性。

拿到下标后,取出table中第i个元素,如果该元素不为空并且该元素取出的引用(e.get()方法的返回值就是),额,这里需要看看Refrence类的源码:

public abstract class Reference<T> {
    
    //从英文注释看出来,该referent根据引用类型不同会被GC区别对待
    private T referent;         /* Treated specially by GC */

    public T get() {
        return this.referent;
    }
   /* -- Constructors -- */

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

    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }
    //省略其他代码
}

其实get()方法无非就是返回当前类的一个泛型对象。而这个类跟我们说的源码的关系就在于Entry继承了WeakRefrence,而WeakRefrence又是Refrence的子类。

Refrence类设计出来的目的就是让咱们自定义的类A继承它,相当于给咱们的类A穿上一层外套,方便垃圾收集器识别何时应该对类A的对象进行回收。

-----------------------------------------------------------------------补充知识开始-----------------------------------------------------------------------

1、对象的引用类型有强软弱虚四种

强引用:A  a = new A();  普通new对象,就是强引用

软引用:new SoftReference(new A()),当内存不够用时,优先收集软引用所占用的内存

弱引用:new WeakReference(new A()),每当发生GC,都会收集该引用指向对象所占用的内存(ThreadLocal的Entry用到了)

虚引用:new PhantomReference(new Object (),QUEUE),垃圾回收也是见一次回收一次,但是回收后,有一个通知到其队列里,用来控制堆外内存回收用。就是当这个引用指向的对象被回收时,虚引用的队列里有一个通知,应该是指向系统内存的引用,再用c语言之类的底层语言回收堆外内存

2、reference指引用本身,referent指的是被包裹的对象(谁继承上图的类,谁就被包裹)

-----------------------------------------------------------------------补充知识结束-----------------------------------------------------------------------

我们前面说到:Entry是ThreadLocalMap的内部类,这个类只有一个属性值,还继承了弱引用,现在说说继承弱引用的作用:

方便每次GC都把Entry对象都给回收掉。

ThreadLocal的结构图表明,Entry是ThreadLocal内部的静态内部类ThreadLocalMap内部的静态内部类,我们看看源码:

    static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        //省略代码无数
   }

源码表明Entry其构造方法调用了super(k),也就是Refrence的构造方法,完成对refrent的赋值。也就是前面e.get()方法的返回值。

那好,既然Entry的构造传入的ThreadLocal的对象k,那么e.get()方法取出来,也应该是ThreadLocal类的一个对象。

回到getEntry的源码:

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

拿到下标后,取出table中第i个元素,如果该元素不为空并且该元素取出的引用(ThreadLocal的一个对象k)也是与ThreadLocal的对象key相等,那么就返回table的第i个元素。

如果e.get()取出的引用与传入的ThreadLocal对象key不相等,那么说明,有可能发生了垃圾回收,弱引用遇到垃圾回收,是像老鼠过街,过一次,被收拾一次。如果发生了垃圾回收,ThreadLocal的get()方法就会调用getEntryAfterMiss方法。

我们看看getEntryAfterMiss方法的源码:

       /**
         * Version of getEntry method for use when key is not found in
         * its direct hash slot.
         *
         * @param  key the thread local object
         * @param  i the table index for key's hash code
         * @param  e the entry at table[i]
         * @return the entry associated with key, or null if no such
         */
        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方法传入的参数有计算好的下标,以及可能为空的e = table[i](或者e.get()取出的值不是当前对象,e.get()此时多半是null)。

1、如果e = table[i]为空,就直接返回为null,表明真的没有该Entry,即总的来说,ThreadLocal中没有该线程存储的私有值。

2、如果e = table[i]不为空,尝试从e.get()方法中取,如果这次判断与k==key(上面有分析),就返回该entry;如果e.get()取出的ThreadLocal变量不为空,又不与传入的key相等,数据过时了(垃圾回收删除了引用),应该获取下一个下标,直到找到当前线程存入的那个key为止。

而nextIndex的实现就简单了:

        /**
         * Increment i modulo len.
         */
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

就是判断当前获取的下标i的下一个是不是小于table的长度,如果小于table的长度,就获取下一个下标,超过table的长度就用0。这里可以看出,Entry数组逻辑上是环形结构

3、最后,取出table[i](代码中的e=tab[i],这里的i已经自增,变了哟),赋值给e,当下一次循环的时候,再次经过上述流程的判断,直到找到相同的key,把table[i]返回。

4、最麻烦的一点,如果e.get()方法获取的弱引用为空,但是此时的e=table[i]不为空,表示有gc活动清除了弱引用,这时应该把所有的老旧的引用都清除,重新为该entry建立新的弱引用。

虽然,ThreadLocal获取线程的私有变量副本看起来不需要参数,但是其内部实现是取出了当前线程对象,利用了当前线程对象的属性threadLocals(ThreadLocalMap类),获取threadLocals的内部类entry对象时,设定了参数this(当前ThreadLocal的对象引用),那实际上是内部的键值存储就是:

ThreadLocal内部存储键值
ThreadLocal当前对象引用(this)存入的Obj

 

现在梳理下threadLocal.get()的流程:

 ThreadLocal之set(T value)

往ThreadLocal里加入线程隔离的私有数据,会用到ThreadLocal的set方法,因为线程隔离,所有set也没有显式的键。

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

前面两行代码都与获取一致,获取到当前线程中的定义在ThreadLocal中的ThreadLocalMap,为空则创建,不为空,就设值。

我们先来看看简单的createMap

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

就是new了一个ThreadLocalMap赋值给当前线程的threadLocals变量。注意,这个指的是ThreadLocal当前对象。结构上面我们上面展示的表格。

再看看map.set(this,value)干了啥:

      private void set(ThreadLocal<?> key, Object value) {
            
            Entry[] tab = table;
            int len = tab.length;
            //利用key计算table的下标i,均匀分布原理如上述【与运算】分析
            int i = key.threadLocalHashCode & (len-1);

            //hash冲突走for循环
            for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
                //取出该entry的弱引用
                ThreadLocal<?> k = e.get();

                //k未被GC删除,并且与key能匹配上,覆盖存值,返回
                if (k == key) {
                    e.value = value;
                    return;
                }
                //k是空的情况,弱引用被清除
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //hash不冲突走这边
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

set分析一:hash不冲突的情况

当hash不冲突的时候,把value放到了一个new出来的Entry里面,在赋值给Entry数组table。

接下来,set方法做了一件事,即:判断是否能清理掉一些table中已经被GC回收的弱引用,如果发现了有Entry存在,但是其弱引用被清除的,那么cleanSomeSlots返回true,不会执行rehash方法。

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;
                    removed = true;
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);//n决定了扫描的次数
            return removed;
        }

while循环即使n传入的值是16,也只循环四次,所有大多数情况,该cleanSomeSlots方法只会循环3到4次(n>>>1 与 n=n/2取整,差不多)。

假如i算出来是8,n是4,那么在逻辑上,n的变化是4-->2-->1-->0,会循环三次,会查找table的9、10、11的entry是否存在以及是否其弱引用被删除。

这时候有两种情况:

1、8号后面仨,肚子装了entry,弱引用也被清除了,这时候set是不会触发rehash的

2、8号后面三,可能entry为空,可能不为空但是弱引用还在(e.get()!=null),这时候cleanSomeSlots返回false,再判断出当前table中如果size大于2/3的table容量时,会触发rehash

在cleanSomeSlots方法中的while循环中,如果ThreadLocal在set时计算的当前下标i对应的下一个下标的table[i]有entry元素,并且其弱引用已被清除,也就是:

 if (e != null && e.get() == null)

这个条件满足,那么就会执行expungeStaleEntry方法,清理掉过时的Entry,这里是直译的stale,我的理解是Entry的弱引用遇到了GC,当entry的弱引用被删除,该entry就称为stale entry。

这里必须看看expungeStaleEntry方法的源码了:

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

            // 清除数组table中给定下标的元素
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // 循环table中的元素,从给定的staleSlot的下一个元素开始,遇到null就rehash
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                //糟糕,下一个元素的弱引用也被清除了
                if (k == null) {
                    // 那就也清除数组table中给定下标的元素
                    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);
                        }
                        //没占用就把e赋值到table新下标的位置中
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

源码中可以知道,expungeStaleEntry的方法不仅删除给定下标的元素,连带着该下标的后续下标也会受到检查,如果也是stale entry,那么会被清除掉,如果不是stale entry就会被重新计算下标再赋值到table中(这步操作不明其意图,如果有看官大佬研究透了,欢迎留言调教),最后会返回【不是连带清除么,清除到哪个位置了?】的下标。

set分析二:hash冲突的情况

源码这么多,您记得住?我可记不住,再看看说到set方法哪里了:

冲突的时候也分两种情况:

1、table中的i下标位置虽然有entry,但是i的nextIndex的弱引用与当前key一致,就进行覆盖操作

2、i的nextIndex的弱引用被清除,那么就执行replaceStaleEntry方法

stale entry即上述的【我的理解是Entry的弱引用遇到了GC,当entry的弱引用被删除,该entry就称为stale entry】,从方法名来看,是替换掉stale entry,我们来一探究竟:

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

            // 往table数组的给定下标的前面找,只要该位置有值,弱引用被清除,就记录下该位置
            // 重复找,slotToExpunge的位置只记录往前找到的最后一个
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            // 往table数组的给定下标的后面找,只要不为空
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                // 如果k和key相等,就替换掉k中的entry的value为新传入的value
                // 并且把table中的staleSlot和staleSlot的下一个交换
                if (k == key) {
                    //e是过时槽的下一个槽,先附上值value
                    e.value = value;
                    //下一个槽保存了过时槽的数据,等待被删除
                    tab[i] = tab[staleSlot];
                    //过时槽装入过时槽下一个槽的数据,交换完成,可以清除过时槽的entry了
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // If we didn't find stale entry on backward scan, the
                // first stale entry seen while scanning for key is the
                // first still present in the run.
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // If key not found, put new entry in stale slot
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }
replaceStaleEntry方法弊端是:会清除给定下标i附近所有的连续的stale entry,如下图:假如staleSlot(也就是某个下标)是4,那么2、3、4、5、6都会被清除掉

至此,ThreadLocal的源码算是大部分看完了。打个哈欠,睡觉。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值