• 转 :ThreadLocal系列(一)-ThreadLocal的使用及原理解析


    转:https://www.cnblogs.com/hama1993/p/10382523.html

    项目中我们如果想要某个对象在程序运行中的任意位置获取到,就需要借助ThreadLocal来实现,这个对象称作线程的本地变量,下面就介绍下ThreadLocal是如何做到线程内本地变量传递的,

    一、基本使用

    先来看下基本用法:

    private static ThreadLocal tl = new ThreadLocal<>();
    
        public static void main(String[] args) throws Exception {
            tl.set(1);
            System.out.println(String.format("当前线程名称: %s, main方法内获取线程内数据为: %s",
                    Thread.currentThread().getName(), tl.get()));
            fc();
            new Thread(ThreadLocalTest::fc).start();
        }
    
        private static void fc() {
            System.out.println(String.format("当前线程名称: %s, fc方法内获取线程内数据为: %s",
                    Thread.currentThread().getName(), tl.get()));
        }

     运行结果:

    当前线程名称: main, main方法内获取线程内数据为: 1
    当前线程名称: main, fc方法内获取线程内数据为: 1
    当前线程名称: Thread-0, fc方法内获取线程内数据为: null

     可以看到,main线程内任意地方都可以通过ThreadLocal获取到当前线程内被设置进去的值,而被异步出去的fc调用,却由于替换了执行线程,而拿不到任何数据值,那么我们现在再来改造下上述代码,在异步发生之前,给Thread-0线程也设置一个上下文数据:

    private static ThreadLocal tl = new ThreadLocal<>();
    
        public static void main(String[] args) throws Exception {
            tl.set(1);
            System.out.println(String.format("当前线程名称: %s, main方法内获取线程内数据为: %s",
                    Thread.currentThread().getName(), tl.get()));
            fc();
            new Thread(()->{
                tl.set(2); //在子线程里设置上下文内容为2
                fc();
            }).start();
            Thread.sleep(1000L); //保证下面fc执行一定在上面异步代码之后执行
            fc(); //继续在主线程内执行,验证上面那一步是否对主线程上下文内容造成影响
        }
    
        private static void fc() {
            System.out.println(String.format("当前线程名称: %s, fc方法内获取线程内数据为: %s",
                    Thread.currentThread().getName(), tl.get()));
        }

     运行结果为:

    当前线程名称: main, main方法内获取线程内数据为: 1 
    当前线程名称: main, fc方法内获取线程内数据为: 1
    当前线程名称: Thread-0, fc方法内获取线程内数据为: 2
    当前线程名称: main, fc方法内获取线程内数据为: 1

    可以看到,主线程和子线程都可以获取到自己的那份上下文里的内容,而且互不影响。

    二、原理分析

    ok,上面通过一个简单的例子,我们可以了解到ThreadLocal(以下简称TL)具体的用法,这里先不讨论它实质上能给我们带来什么好处,先看看其实现原理,等这些差不多了解完了,我再通过我曾经做过的一个项目,去说明TL的作用以及在企业级项目里的用处。

    我以前在不了解TL的时候,想着如果让自己实现一个这种功能的轮子,自己会怎么做,那时候的想法很单纯,觉得通过一个Map就可以解决,Map的key设置为Thread.currentThread(),value设置为当前线程的本地变量即可,但后来想想就觉得不太现实了,实际项目中可能存在大量的异步线程,对于内存的开销是不可估量的,而且还有个严重的问题,线程是运行结束后就销毁的,如果按照上述的实现方案,map内是一直持有这个线程的引用的,导致明明执行结束的线程对象不能被jvm回收,造成内存泄漏,时间久了,会直接OOM。

    所以,java里的实现肯定不是这么简单的,下面,就来看看java里的具体实现吧。

    先来了解下,TL的基本实现,为了避免上述中出现的问题,TL实际上是把我们设置进去的值以k-v的方式放到了每个Thread对象内(TL对象做k,设置的值做v),也就是说,TL对象仅仅起到一个标记、对Thread对象维护的map赋值的作用。

    先从set方法看起:

    public void set(T value) {
            Thread t = Thread.currentThread(); //获取当前线程
            ThreadLocal.ThreadLocalMap map = getMap(t); //获取到当前线程持有的ThreadLocalMap对象
            if (map != null)
                map.set(this, value); //直接set值,具体方法在下面
            else
                createMap(t, value); // 为空就给当前线程创建一个ThreadLocalMap对象,赋值给Thread对象,具体方法在下面
        }
    
        ThreadLocal.ThreadLocalMap getMap(Thread t) {
            return t.threadLocals; //每个线程都有一个ThreadLocalMap,key为TL对象(其实是根据对象hash计算出来的值),value为该线程在此TL对象下存储的内容值
        }
    
        private void set(ThreadLocal<?> key, Object value) {
    
            ThreadLocal.ThreadLocalMap.Entry[] tab = table; //获取存储k-v对象的数组(散列表)
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1); //根据TL对象的hashCode(也是特殊计算出来的,保证每个TL对象的hashCode不同)计算出下标
    
            for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) { //线性探查法解决哈希冲突问题,发现下标i已经有Entry了,则就查看i+1位置处是否有值,以此类推
                ThreadLocal<?> k = e.get(); //获取k
    
                if (k == key) { //若k就是当前TL对象,则直接为其value赋值
                    e.value = value;
                    return;
                }
    
                if (k == null) { //若k为空,则认为是可回收的Entry,则利用当前k和value组成新的Entry替换掉该可回收Entry
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
    
            //for循环执行完没有终止程序,说明遇到了空槽,这个时候直接new对象赋值即可
            tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold) //这里用来清理掉k为null的废弃Entry
                rehash(); //如果没有发生清除Entry并且size超过阈值(阈值 = 最大长度 * 2/3),则进行扩容
        }
    
    
        //直接为当前Thread初始化它的ThreadLocalMap对象
        void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
        }
    
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY]; //初始化数组
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //计算初始位置
            table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue); //因为初始化不存在hash冲突,直接new
            size = 1;
            setThreshold(INITIAL_CAPACITY); //给阈值赋值,上面已经提及,阈值 = 最大长度 * 2/3
        }

     通过上述代码,我们大致了解了TL在set值的时候发生的一些操作,结合之前说的,我们可以确定的是,TL其实对于线程来说,只是一个标识,而真正线程的本地变量被保存在每个线程对象的ThreadLocalMap里,这个map里维护着一个Entry[]的数组(散列表),Entry是个k-v结构的对象(如图1-1),k为TL对象,v为对应TL保存在该线程内的本地变量值,值得注意的是,这里的k针对TL对象的引用是个弱引用,来看下源码:

    static class Entry extends WeakReference<ThreadLocal<?>> {
                /** The value associated with this ThreadLocal. */
                Object value;
    
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }

    为什么这里需要弱引用呢?我们先来看一张图,结合上面的介绍和这张图,来了解TL和Thread间的关系:

    图1-1

    图中虚线表示弱引用,那么为什么要这么做呢?

    简单来说,一个TL对象被创建出来,并且被一个线程放到自己的ThreadLocalMap里,假如TL对象失去原有的强引用,但是该线程还没有死亡,如果k不是弱引用,那么就意味着TL并不能被回收,现在k为弱引用,那么在TL失去强引用的时候,gc可以直接回收掉它,弱引用失效,这就是上面代码里会进行检查,k=null的清除释放内存的原因(这个可以参考下面expungeStaleEntry方法,而且set、get、remove都会调用该方法,这也是TL防止内存泄漏所做的处理)。

    综上,简单来说这个弱引用就是用来解决由于使用TL不当导致的内存泄漏问题的,假如没有弱引用,那么你又用到了线程池(池化后线程不会被销毁),然后TL对象又是局部的,那么就会导致线程池内线程里的ThreadLocalMap存在大量的无意义的TL对象引用,造成过多无意义的Entry对象,因为即便调用了set、get等方法检查k=null,也没有作用,这就导致了内存泄漏,长时间这样最终可能导致OOM,所以TL的开发者为了解决这种问题,就将ThreadLocalMap里对TL对象的引用改为弱引用,一旦TL对象失去强引用,TL对象就会被回收,那么这里的弱引用指向的值就为null,结合上面说的,调用操作方法时会检查k=null的Entry进行回收,从而避免了内存泄漏的可能性。

    因为TL解决了内存泄漏的问题,因此即便是局部变量的TL对象且启用线程池技术,也比较难造成内存泄漏的问题,而且我们经常使用的场景就像一开始的示例代码一样,会初始化一个全局的static的TL对象,这就意味着该对象在程序运行期间都不会存在强引用消失的情况,我们可以利用不同的TL对象给不同的Thread里的ThreadLocalMap赋值,通常会set值(覆盖原有值),因此在使用线程池的时候也不会造成问题,异步开始之前set值,用完以后remove,TL对象可以多次得到使用,启用线程池的情况下如果不这样做,很可能业务逻辑也会出问题(一个线程存在之前执行程序时遗留下来的本地变量,一旦这个线程被再次利用,get时就会拿到之前的脏值);

    说完了set,我们再来看下get:

    public T get() {
            Thread t = Thread.currentThread();
            ThreadLocal.ThreadLocalMap map = getMap(t); //获取线程内的ThreadLocalMap对象
            if (map != null) {
                ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this); //根据当前TL对象(key)获取对应的Entry
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;
                    return result; //直接返回value即可
                }
            }
            return setInitialValue(); //如果发现当前线程还没有ThreadLocalMap对象,则进行初始化
        }
    
        private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1); //计算下标
            ThreadLocal.ThreadLocalMap.Entry e = table[i];
            if (e != null && e.get() == key) //根据下标获取的Entry对象如果key也等于当前TL对象,则直接返回结果即可
                return e;
            else
                return getEntryAfterMiss(key, i, e); //上面说过,有些情况下存在下标冲突的问题,TL是通过线性探查法来解决的,所以这里也一样,如果上面没找到,则继续通过下标累加的方式继续寻找
        }
    
        private ThreadLocal.ThreadLocalMap.Entry getEntryAfterMiss(ThreadLocal<?> key, int i, ThreadLocal.ThreadLocalMap.Entry e) {
            ThreadLocal.ThreadLocalMap.Entry[] tab = table;
            int len = tab.length;
    
            while (e != null) {
                ThreadLocal<?> k = e.get(); //继续累加下标的方式一点点的往下找
                if (k == key) //找到了就返回出去结果
                    return e;
                if (k == null) //这里也会检查k==null的Entry,满足就执行删除操作
                    expungeStaleEntry(i);
                else //否则继续累加下标查找
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null; //找不到返回null
        }
    
    
        //这里也放一下nextIndex方法
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

     最后再来看看remove方法:

    public void remove() {
            ThreadLocal.ThreadLocalMap m = getMap(Thread.currentThread());
            if (m != null)
                m.remove(this); //清除掉当前线程ThreadLocalMap里以当前TL对象为key的Entry
        }
    
        private void remove(ThreadLocal<?> key) {
            ThreadLocal.ThreadLocalMap.Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1); //计算下标
            for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) { //找到目标Entry
                    e.clear(); //清除弱引用
                    expungeStaleEntry(i); //通过该方法将自己清除
                    return;
                }
            }
        }
    
        private int expungeStaleEntry(int staleSlot) { //参数为目标下标
            ThreadLocal.ThreadLocalMap.Entry[] tab = table;
            int len = tab.length;
    
            tab[staleSlot].value = null; //首先将目标value清除
            tab[staleSlot] = null;
            size--;
    
            // Rehash until we encounter null
            ThreadLocal.ThreadLocalMap.Entry e;
            int i;
            // 由目标下标开始往后逐个检查,k==null的清除掉,不等于null的要进行rehash
            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;
    
                        // 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;
        }

     目前主要方法set、get、remove已经介绍完了,包含其内部存在的弱引用的作用,以及实际项目中建议的用法,以及为什么要这样用,也进行了简要的说明,下面一篇会进行介绍InheritableThreadLocal的用法以及其原理性分析。

  • 相关阅读:
    历史博文汇总
    Golang快速入门:从菜鸟到大佬
    网络1911、1912 DS第4次作业--批改总结
    [机器学习实战-Logistic回归]使用Logistic回归预测各种实例
    [机器学习实战-kNN算法]约会网站问题和手写数字系统实例
    SQL学习笔记(一)之基础语句
    Python学习笔记(九)之面向对象编程(下)
    Python学习笔记(八)之面向对象编程(上)
    Python学习笔记(七)之Python模块
    Python学习笔记(六)之函数式编程
  • 原文地址:https://www.cnblogs.com/cb1186512739/p/14214285.html
Copyright © 2020-2023  润新知