• ThreadLocal源码解析-Java8


    目录

    一.ThreadLocal介绍

      1.1 ThreadLocal的功能

      1.2 ThreadLocal使用示例

    二.源码分析-ThreadLocal

      2.1 ThreadLocal的类层级关系

      2.2 ThreadLocal的属性字段

      2.3 创建ThreadLocal对象

      2.4 ThreadLocal-set操作

      2.5 ThreadLocal-get操作

      2.6 ThreadLocal-remove操作

    三.ThreadLocalMap类

      3.0 线性探测算法解决hash冲突

      3.1 Entry内部类

      3.2 ThreadLocalMap的常量介绍

      3.3 实例化ThreadLocalMap

      3.4 ThreadLocalMap的set操作

      3.5 清理陈旧Entry和rehash

    四.总结 

    一.介绍ThreadLocal

    1.1ThreadLocal的功能

      我们知道,变量从作用域范围进行分类,可以分为“全局变量”、“局部变量”两种:

      1.全局变量(global variable),比如类的静态属性(加static关键字),在类的整个生命周期都有效;

      2.局部变量(local variable),比如在一个方法中定义的变量,作用域只是在当前方法内,方法执行完毕后,变量就销毁(释放)了;

      使用全局变量,当多个线程同时修改静态属性,就容易出现并发问题,导致脏数据;而局部变量一般来说不会出现并发问题(在方法中开启多线程并发修改局部变量,仍可能引起并发问题);

      再看ThreadLocal,可以用来保存局部变量,只不过这个“局部”是指“线程”作用域,也就是说,该变量在该线程的整个生命周期中有效。

      关于ThreadLocal的使用场景,可以查看ThreadLocal的使用场景分析

     

    1.2ThreadLocal的使用示例

      ThreadLocal使用非常简单。

    package cn.ganlixin;
    
    import org.junit.Test;
    
    import java.util.Arrays;
    import java.util.List;
    
    public class TestThreadLocal {
    
        private static class Goods {
            public Integer id;
            public List<String> tags;
        }
    
        @Test
        public void testReference() {
            Goods goods1 = new Goods();
            goods1.id = 10;
            goods1.tags = Arrays.asList("healthy", "cheap");
    
            ThreadLocal<Goods> threadLocal = new ThreadLocal<>();
            threadLocal.set(goods1);
    
            Goods goods2 = threadLocal.get();
            System.out.println(goods1); // cn.ganlixin.TestThreadLocal$Goods@1c655221
            System.out.println(goods2); // cn.ganlixin.TestThreadLocal$Goods@1c655221
    
            goods2.id = 100;
            System.out.println(goods1.id);  // 100
            System.out.println(goods2.id);  // 100
    
            threadLocal.remove();
            System.out.println(threadLocal.get()); // null
        }
    
        @Test
        public void test2() {
            // 一个线程中,可以创建多个ThreadLocal对象,多个ThreadLoca对象互不影响
            ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
            ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
            // ThreadLocal存的值默认为null
    
            System.out.println(threadLocal1.get()); // null
    
            threadLocal1.set("this is value1");
            threadLocal2.set("this is value2");
            System.out.println(threadLocal1.get()); // this is value1
            System.out.println(threadLocal2.get());  // this is value2
    
            // 可以重写initialValue进行设置初始值
            ThreadLocal<String> threadLocal3 = new ThreadLocal<String>() {
                @Override
                protected String initialValue() {
                    return "this is initial value";
                }
            };
            System.out.println(threadLocal3.get()); // this is initial value
        }
    }
    

      

    二.源码分析-ThreadLocal

    2.1ThreadLocal类层级关系

      

      ThreadLocal类中有一个内部类ThreadLocalMap,这个类特别重要,ThreadLocal的各种操作基本都是围绕ThreadLocalMap进行的

      对于ThreadLocalMap有来说,它内部定义了一个Entry内部类,有一个table属性,是一个Entry数组,他们有一些相似的地方,但是ThreadLocalMap和HashMap并没有什么关系。

      先大概看一下内存关系图,不理解也没关系,看了后面的代码应该就能理解了:

       

      大概解释一下,栈中的Thread ref(引用)堆中的Thread对象,Thread对象有一个属性threadlocals(ThreadLocalMap类型),这个Map中每一项(Entry)的value是ThreadLocal.set()的值,而Map的key则是ThreadLocal对象。

      下面在介绍源码的时候,会从两部分进行介绍,先介绍ThreadLocal的常用api,然后再介绍ThreadLocalMap,因为ThreadLocal的api内部其实都是在操作ThreadLocalMap,所以看源码时一定要知道他们俩之间的关系

    2.2ThreadLocal的属性

      ThreadLocal有3个属性,主要的功能就是生成ThreadLocal的hash值。

    // threadLocalHashCode用来表示当前ThreadLocal对象的hashCode,通过计算获得
    private final int threadLocalHashCode = nextHashCode();
    
    // 一个AtomicInteger类型的属性,功能就是计数,各种操作都是原子性的,在并发时不会出现问题
    private static AtomicInteger nextHashCode = new AtomicInteger();
    
    // hash值的增量,不是随便指定的,被称为“黄金分割数”,能让hash结果均衡分布
    private static final int HASH_INCREMENT = 0x61c88647;
    
    /**
     * 通过计算,为当前ThreadLocal对象生成一个HashCode
     */
    private static int nextHashCode() {
        // 获取当前nextHashCode,然后递增HASH_INCREMENT
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    

      

    2.3创建ThreadLocal对象

      ThreadLocal类,只有一个无参构造器,如果需要是指默认值,则可以重写initialValue方法:

    public ThreadLocal() {}
    
    /**
     * 初始值默认为null,要设置初始值,只需要设置为方法返回值即可
     *
     * @return ThreadLocal的初始值
     */
    protected T initialValue() {
        return null;
    }
    

      需要注意的是initialValue方法并不会在创建ThreadLocal对象的时候设置初始值,而是延迟执行:当ThreadLocal直接调用get时才会触发initialValue执行(get之前没有调用set来设置过值),initialValue方法在后面还会介绍。 

    2.4ThreadLocal-set操作

      下面这段代码只给出了ThreadLocal的set代码:

    public void set(T value) {
        // 获取当前线程
        Thread t = Thread.currentThread();
    
        // 获取当前线程的ThreadLocalMap属性,ThreadLocal有一个threadLocals属性(ThreadLocalMap类型)
        ThreadLocalMap map = getMap(t);
    
        if (map != null) {
            // 如果当前线程有关联的ThreadLocalMap对象,则调用ThreadLocalMap的set方法进行设置
            map.set(this, value);
        } else {
            // 创建一个与当前线程关联的ThreadLocalMap对象,并设置对应的value
            createMap(t, value);
        }
    }
    
    /**
     * 获取线程关联的ThreadLocalMap对象
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    /**
     * 创建ThreadLocalMap
     * @param t          key为当前线程
     * @param firstValue value为ThreadLocal.set的值
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

      如果想立即了解ThreadLocalMap的set方法,则可点此跳转

    2.5ThreadLocal-get操作

      前面说过“重写ThreadLocal的initialValue方法来设置ThreadLocal的默认值,并不是在创建ThreadLocal的时候执行的,而是在直接get的时候执行的”,看了下面的代码,就知道这句话的具体含义了,感觉设计很巧妙:

    public T get() {
        // 获取当前线程
        Thread t = Thread.currentThread();
    
        // 获取当前线程对象的threadLocals属性
        ThreadLocalMap map = getMap(t);
    
        // 若当前线程对象的threadLocals属性不为空(map不为空)
        if (map != null) {
            // 当前ThreadLocal对象作为key,获取ThreadLocalMap中对应的Entry
            ThreadLocalMap.Entry e = map.getEntry(this);
    
            // 如果找到对应的Entry,则证明该线程的该ThreadLocal有值,返回值即可
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T) e.value;
                return result;
            }
        }
    
        // 1.当前线程对象的threadLocals属性为空(map为空)
        // 2.或者map不为空,但是未在map中查询到以该ThreadLocal对象为key对应的entry
        // 这两种情况,都会进行设置初始值,并将初始值返回
        return setInitialValue();
    }
    
    /**
     * 设置ThreadLocal初始值
     *
     * @return 初始值
     */
    private T setInitialValue() {
        // 调用initialValue方法,该方法可以在创建ThreadLocal的时候重写
        T value = initialValue();
        Thread t = Thread.currentThread();
    
        // 获取当前线程的threadLocals属性(map)
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // threadLocals属性值不为空,则进行调用ThreadLocalMap的set方法
            map.set(this, value);
        } else {
            // 没有关联的threadLocals,则创建ThreadLocalMap,并在map中新增一个Entry
            createMap(t, value);
        }
    
        // 返回初始值
        return value;
    }
    
    /**
     * 初始值默认为null,要设置初始值,只需要设置为方法返回值即可
     * 创建ThreadLocal设置默认值,可以覆盖initialValue方法,initialValue方法不是在创建ThreadLocal时执行,而是这个时候执行
     *
     * @return ThreadLocal的初始值
     */
    protected T initialValue() {
        return null;
    }
    

         

    2.6ThreadLocal-remove操作

      一般是在ThreadLocal对象使用完后,调用ThreadLocal的remove方法,在一定程度上,可以避免内存泄露;

    /**
     * 删除当前线程中threadLocals属性(map)中的Entry(以当前ThreadLocal为key的)
     */
    public void remove() {
        // 获取当前线程的threadLocals属性(ThreadLocalMap)
        ThreadLocalMap m = getMap(Thread.currentThread());
    
        if (m != null) {
            // 调用ThreadLocalMap的remove方法,删除map中以当前ThreadLocal为key的entry
            m.remove(this);
        }
    }

    三.ThreadLocalMap内部类

    3.0 线性探测算法解决hash冲突

      在介绍ThreadLocalMap的之前,强烈建议先了解一下线性探测算法,这是一种解决Hash冲突的方案,如果不了解这个算法就去看ThreadLocalMap的源码就会非常吃力,会感到莫名其妙。

      链接在此:利用线性探测法解决hash冲突

    3.1Entry内部类

      ThreadLocalMap是ThreadLocal的内部类,ThreadLocalMap底层使用数组实现,每一个数组的元素都是Entry类型(在ThreadLocalMap中定义的),源码如下:

    /**
     * ThreadLocalMap中存放的元素类型,继承了弱引用类
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        // key对应的value,注意key是ThreadLocal类型
        Object value;
    
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

      ThreadLocalMap和HashMap类似,比较一下:

      a:底层都是使用数组实现,数组元素类型都是内部定义,Java8中,HashMap的元素是Node类型(或者TreeNode类型),ThreadLocalMap中的元素类型是Entry类型;

      b.都是通过计算得到一个值,将这个值与数组的长度(容量)进行与操作,确定Entry应该放到哪个位置;

      c.都有初始容量、负载因子,超过扩容阈值将会触发扩容;但是HashMap的初始容量、负载因子是可以更改的,而ThreadLocalMap的初始容量和负载因子不可修改;

      注意Entry继承自WeakReference类,在实例化Entry时,将接收的key传给父类构造器(也就是WeakReference的构造器),WeakReference构造器又将key传给它的父类构造器(Reference):

    // 创建Reference对象,接受一个引用
    Reference(T referent) {
        this(referent, null);
    }
    
    // 设置引用
    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }
    

      关于Java的各种引用,可以参考:Java-强引用、软引用、弱引用、虚引用

    3.2ThreadLocalMap的常量介绍

    // ThreadLocalMap的初始容量
    private static final int INITIAL_CAPACITY = 16;
    
    // ThreadLocalMap底层存数据的数组
    private Entry[] table;
    
    // ThreadLocalMap中元素的个数
    private int size = 0;
    
    // 扩容阈值,当size达到阈值时会触发扩容(loadFactor=2/3;newCapacity=2*oldCapacity)
    private int threshold; // Default to 0
    

      

    3.3创建ThreadLocalMap对象

      创建ThreadLocalMap,是在第一次调用ThreadLocal的set或者get方法时执行,其中第一次未set值,直接调用get时,就会利用ThreadLocal的初始值来创建ThreadLocalMap。

      ThreadLocalMap内部类的源码如下:

    /**
     * 初始化一个ThreadLocalMap对象(第一次调用ThreadLocal的set方法时创建),传入ThreadLocal对象和对应的value
     */
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        // 创建一个Entry数组,容量为16(默认)
        table = new Entry[INITIAL_CAPACITY];
    
        // 计算新增的元素,应该放到数组的哪个位置,根据ThreadLocal的hash值与初始容量进行"与"操作
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    
        // 创建一个Entry,设置key和value,注意Entry中没有key属性,key属性是传给Entry的父类WeakReference
        table[i] = new Entry(firstKey, firstValue);
    
        // 初始容量为1
        size = 1;
    
        // 设置扩容阈值
        setThreshold(INITIAL_CAPACITY);
    }
    
    /**
     * 设置扩容阈值,接收容量值,负载因子固定为2/3
     */
    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }

    3.4 ThreadLocalMap的set操作

      ThreadLocal的set方法,其实核心就是调用ThreadLocalMap的set方法,set方法的流程比较长

    /**
     * 为当前ThreadLocal对象设置value
     */
    private void set(ThreadLocal<?> key, Object value) {
        Entry[] tab = table;
        int len = tab.length;
    
        // 计算新元素应该放到哪个位置(这个位置不一定是最终存放的位置,因为可能会出现hash冲突)
        int i = key.threadLocalHashCode & (len - 1);
    
        // 判断计算出来的位置是否被占用,如果被占用,则需要找出应该存放的位置
        for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
            // 获取Entry中key,也就是弱引用的对象
            ThreadLocal<?> k = e.get();
    
            // 判断key是否相等(判断弱引用的是否为同一个ThreadLocal对象)如果是,则进行覆盖
            if (k == key) {
                e.value = value;
                return;
            }
    
            // k为null,也就是Entry的key已经被回收了,当前的Entry是一个陈旧的元素(stale entry)
            if (k == null) {
                // 用新元素替换掉陈旧元素,同时也会清理其他陈旧元素,防止内存泄露
                replaceStaleEntry(key, value, i);
                return;
            }
        }
    
        // map中没有ThreadLocal对应的key,或者说没有找到陈旧的Entry,则创建一个新的Entry,放入数组中
        tab[i] = new Entry(key, value);
        // ThreadLocalMap的元素数量加1
        int sz = ++size;
    
        // 先清理map中key为null的Entry元素,该Entry也应该被回收掉,防止内存泄露
        // 如果清理出陈旧的Entry,那么就判断是否需要扩容,如果需要的话,则进行rehash
        if (!cleanSomeSlots(i, sz) && sz >= threshold) {
            rehash();
        }
    }

      上面最后几行代码涉及到清理陈旧Entry和rehash,这两块的代码在下面。

    3.5清理陈旧Entry和rehash

      陈旧的Entry,是指Entry的key为null,这种情况下,该Entry是不可访问的,但是却不会被回收,为了避免出现内存泄漏,所以需要在每次get、set、replace时,进行清理陈旧的Entry,下面只给出一部分代码:

    /**
     * 清理map中key为null的Entry元素,该Entry也应该被回收掉,防止内存泄露
     *
     * @param i 新Entry插入的位置
     * @param n 数组中元素的数量
     * @return 是否有陈旧的entry的清除
     */
    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);
        return removed;
    }
    
    private void rehash() {
        // 清除底层数组中所有陈旧的(stale)的Entry,也就是key为null的Entry
        // 同时每清除一个Entry,就对其后面的Entry重新计算hash,获取新位置,使用线性探测法,重新确定最终位置
        expungeStaleEntries();
    
        // 清理完陈旧Entry后,判断是否需要扩容
        if (size >= threshold - threshold / 4) {
            // 扩容时,容量变为旧容量的2倍,再进行rehash,并使用线性探测发确定Entry的新位置
            resize();
        }
    }
    

      在rehash的时候,涉及到“线性探测法”,是一种用来解决hash冲突的方案,可以查看利用线性探测法解决hash冲突了解详情。

    3.6ThreadLocalMap-remove操作

      remove操作,是调用ThreadLocal.remove()方法时,删除当前线程的ThreadLocalMap中该ThreadLocal为key的Entry。

    /**
     * 移除当前线程的threadLocals属性中key为ThreadLocal的Entry
     *
     * @param key 要移除的Entry的key(ThreadLocal对象)
     */
    private void remove(ThreadLocal<?> key) {
        Entry[] tab = table;
        int len = tab.length;
    
        // 计算出该ThreadLocal对应的key应该存放的位置
        int i = key.threadLocalHashCode & (len - 1);
    
        // 找到指定位置,开始按照线性探测算法进行查找到该Thread的Entry
        for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
    
            // 如果Entry的key相同
            if (e.get() == key) {
                // 调用WeakReference的clear方法,Entry的key是弱引用,指向ThreadLocal,现在将key指向null
                // 则该ThreadLocal对象在会在下一次gc时,被垃圾收集器回收
                e.clear();
    
                // 将该位置的Entry中的value置为null,于是value引用的对象也会被垃圾收集器回收(不会造成内存泄漏)
                // 同时内部会调整Entry的顺序(开放探测算法的特点,删除元素后会重新调整顺序)
                expungeStaleEntry(i);
    
                return;
            }
        }
    }

    四.总结

      在学习ThreadLocal类源码的过程还是受益颇多的:

      1.ThreadLocal的使用场景;

      2.initialValue的延迟执行;

      3.HashMap使用链表+红黑树解决hash冲突,ThreadLocalMap使用线性探测算法(开放寻址)解决hash冲突

      另外,ThreadLocal还有一部分内容,是关于弱引用和内存泄漏的问题,我会继续写一篇博客进行总结。

      原文地址:https://www.cnblogs.com/-beyond/p/13093032.html

  • 相关阅读:
    区块链学习(2)钱包
    区块链学习(1)密钥,公钥和地址
    Ubuntu下安装和开启telnet
    ubuntu下的ppa源使用
    tensorflow中手写识别笔记
    交叉熵解读
    Ubuntu下对executable (application/x-executable)文件创建快捷方式
    Numpy学习笔记(四)
    pycharm问题总结
    Numpy学习笔记(三)
  • 原文地址:https://www.cnblogs.com/-beyond/p/13093032.html
Copyright © 2020-2023  润新知