热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

ThreadLocal从源码角度简单分析

目录ThreadLcoal源码浅析ThreadLocal的垃圾回收Java引用ThreadLocal的回收各

目录

  • ThreadLcoal源码浅析
  • ThreadLocal的垃圾回收
    • Java引用
    • ThreadLocal的回收
    • 各线程中threadLocalMap的回收
    • 内存泄露问题
  • 总结
  • 参考

ThreadLcoal源码浅析

我们知道ThreadLocal用于维护多个线程线程独立的变量副本,这些变量只在线程内共享,可跨方法、类等,如下是一个维护多个线程Integer变量的ThreadLocal:

ThreadLocal threadLocalNum = new ThreadLocal<>();

每个使用threadLocalNum的线程,可以通过形如threadLocalNum.set(1)的方式创建了一个独立使用的Integer变量副本,那么它是怎么实现的呢?我们今天就来简单的分析一下。

先看下ThreadLocal的set方法是如何实现的,源码如下:

public void set(T value) {
        Thread t = Thread.currentThread();  //获取当前线程
        ThreadLocalMap map = getMap(t);     //获取当前线程的ThreadLocalMap
        if (map != null)
            map.set(this, value);           //当前线程的ThreadLocalMap不为空则直接设值
        else
            createMap(t, value);            //当前线程的ThreadLocalMap为空则创建一个来设置值
    }

是的,你没有看错,是获取当前线程中的ThreadLocalMap来设置的值,我们来看一下getMap(t)是如何实现的:

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

然后我们看到Thread中包含了一个ThreadLocalMap类型的属性:

ThreadLocal.ThreadLocalMap threadLocals = null;

到这里我们可以得出一个结论:各个线程持有了一个ThreadLocalMap的属性,通过ThreadLocal设置变量时,直接设置到了对应线程的的ThreadLocalMap属性中

那么不同的线程中通过ThreadLocal设置的值是如何关联定义的ThreadLocal变量和Thread中的ThreadLocalMap的呢?我们接着分析。

前面写到当前线程的ThreadLocalMap为空则创建一个ThreadLocalMap来设值,我们来看下createMap(t, value)的具体实现:

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

///////////////////
//ThreadLocalMap构造器定义如下
ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);  //
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
private static final int INITIAL_CAPACITY = 16;

线程中threadLocals是一个ThreadLocalMap变量,其默认值是null,该线程在首次使用threadLocal对象调用set的时候通过createMap(Thread t, T firstValue)实例化。

先来看一下ThreadLocalMap,它是在ThreadLocal中定义的一个静态内部类,其内属性如下:

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

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

        /**
         * The number of entries in the table.
         */
        private int size = 0;

        /**
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0

其中属性private Entry[] table,用于存储通过threadLocal set 进来的变量,Entry定义如下:

static class Entry extends WeakReference> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

Entry继承了WeakReference>,ThreadLocal在构造器中被指定为弱引用super(k)(后面会单独讨论为何这里使用弱引用)。

至此,我们可以知道ThreadLocal和Thead的内存结构如下:

ThreadLocal的垃圾回收

网上看到很多文章都在讲ThreadLocal的内存泄露问题,所以也在这里简单说一下自己的理解。

从上面的结构可以看出ThreadLocal涉及到的要回收的对象包括:

  • ThreadLocal实例本身
  • 各线程中的threadLocalMap,其中包括各个Entry的 key, value

下面先简述java的引用,然后分别讨论ThreadLocal本身的回收和threadLcoalMap的回收

Java引用

  • 强引用(StrongReference):对象可达就不会被gc回收,空间不足时报error
  • 软引用(SoftReference):对象无其他强引用,当空间不足时才会被gc回收。
  • 弱引用(WeakReference):对象无其他强引用,gc过程扫描到就会被回收。

ThreadLocal的回收

ThreadLocal实例的引用主要包括两种:

  • ThreadLocal定义处的强引用
  • 各线程中ThreadLocalMap里的key=weak(threadLocal), 是弱引用

强引用还在的情况下ThreadLocal一定不会被回收;无强引用后,由于各个Thread中Entry的key是弱引用,会在下次GC后变为null。ThreadLocal实例什么时候被回收完全取决于强引用何时被干掉,那么什么时候强引用会被销毁呢?最简单的就是 threadLocal=null强引用被赋值为null;其它也可是threadLocal是一个局部变量,在方法退出后引用被销毁,等等。

这里来回答一下前面提到的为什么ThreadLocalMap中将key设计为弱引用,我们假设如果ThreadLocalMap中是强引用会出现什么情况?定义ThreadLocal时定义的强引用被置为null的时候,如果还有其它使用了该ThreadLocal的线程没有完成,还需要很久会执行完成,那么这个线程将一直持有该ThreadLocal实例的引用,直到线程完成,期间ThreadLocal实例都不能被回收,最重要的是如果不了解ThreadLocal内部实现,你可能都不知道还有其他线程引用了threadLocal实例。

线程结束时清除ThreadLocalMap的代码Thread.exit()如下:

   /**
     * This method is called by the system to give a Thread
     * a chance to clean up before it actually exits.
     */
    private void exit() {
        if (group != null) {
            group.threadTerminated(this);
            group = null;
        }
        /* Aggressively null out all reference fields: see bug 4006245 */
        target = null;
        /* Speed the release of some of these resources */
        threadLocals = null;
        inheritableThreadLocals = null;
        inheritedAccessCOntrolContext= null;
        blocker = null;
        uncaughtExceptiOnHandler= null;
    }

所以,对于threadLocal对象本身而言, 只要通过threadLocal=null就可以实现回收了。

各线程中threadLocalMap的回收

单从引用的角度来看,各线程中的threadLocalMap,其中包括各个Entry的key 和 value。线程(也就是Thread实例)本身一直持有threadLocalMap的强引用,只有在线程结束的时候才会被回收。而key是threadLocal对象的弱引用,当threadLocal被置为null时就会被回收,此时的Entry数组中就会出现很多key为null,但是value有值的元素,那么value在threadLocal对象为空后应该怎么回收呢?

ThreadLocal在实现的时候提供了一些方法:set/get/remove,可以在执行它们的时候调用ThreadLocalMap的方法回收ThreadLocalMap中已经失效(key=null)的entry实例。

这里就以set为例看看ThreadLocal是如何回收entry的,ThreadLocal set方法实现如下:

//ThreadLocal
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);  // 本次要分析的方法
        else
            createMap(t, value);   //这里前面已经分析了
    }

//ThreadLocalMap
private void set(ThreadLocal key, Object value) {
            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);  //获取当前threadLocal实例的hashcode,同时也是table的下标

            //这里for循环找key,是因为hash冲突会使hashcode指向的下标不是真实的存储位置
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) { 
                ThreadLocal k = e.get();
                //找到了设置为新值
                if (k == key) {
                    e.value = value;
                    return;
                }
                //entry不为null,key为null
                //说明原来被赋值过,但是原threadLocal已经被回收
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //如果下标对应的entry为null, 则新建一个entry
            tab[i] = new Entry(key, value);
            int sz = ++size;
            //清理threadlocal中其它被回收了的entry(也就是key=null的entry)
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                //rehash
                rehash();
        }

看一下cleanSomeSlots的实现:

//ThreadLocalMap
private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                //获取下一个entry的下标
                i = nextIndex(i, len);
                Entry e = tab[i];
                //entry不为null,key为null
                //说明原来被赋值过,但是原threadLocal已经被回收
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    // 删除已经无效的entry
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }



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

            // 回收无效entry
            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();
                //entry不为null,key为null,应该回收
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    //rehash的实现
                    //计算当前entry的k的hashcode,看是下标是否应该为i
                    //如果不为i说明,是之前hash冲突放到这儿的,现在需要reash
                    int h = k.threadLocalHashCode & (len - 1);
                    //h!=i 说明hash冲突了, entry不应该放在下标为i的位置
                    if (h != i) {
                        tab[i] = null;
                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        //找正确的位置h,但是还是有可能冲突所以要循环
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

从上面的分析我们可以看到把ThreadLocalMap中的key设计为weakReference,也使set方法可以通过key==null && entry != null判断entry是否失效

总结一下ThreadLocal set方法的实现:

  • 根据threadLocal计算hashcode找到entry[]数组对应位置设置值
  • 遍历数组找到其它失效的(entry不为null,key为null)的entry删除

内存泄露问题

ThreadLocal通过巧妙的设计最大程度上减少了内存泄露的可能,但是并没有完全消除。

当我们使用完ThreadLocal后没有调用set/get/remove方法,那么可能会导致失效内存不能及时被回收,导致内存泄露,尤其是在value占用内存较大的情况。

所以最佳实践是,在明确ThreadLocal不再使用时,手动调用remove方法及时清空。

总结

  • ThreadLocal 并不解决线程间共享数据的问题
  • ThreadLocal是通过让线程内的ThreadLocalMap.Entry的key指向自身,来实现了对线程内对象的引用,从而可以在线程内方便的使用变量。同时因为操作的都是线程内的变量,也避免了实例线程安全的问题
  • ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
  • ThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用,避免了 ThreadLocal 对象无法被回收的问题
  • ThreadLocalMap 的 set 方法通过调用 cleanSomeSlots 方法回收键为 null 的 Entry 对象的值(即失效实例)从而防止内存泄漏(其它的remove,get类似)
  • 在明确ThreadLocal不再使用时,手动调用remove方法及时清空

参考

正确理解Thread Local的原理与适用场景


推荐阅读
  • 本文介绍了Swing组件的用法,重点讲解了图标接口的定义和创建方法。图标接口用来将图标与各种组件相关联,可以是简单的绘画或使用磁盘上的GIF格式图像。文章详细介绍了图标接口的属性和绘制方法,并给出了一个菱形图标的实现示例。该示例可以配置图标的尺寸、颜色和填充状态。 ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • 本文介绍了如何使用C#制作Java+Mysql+Tomcat环境安装程序,实现一键式安装。通过将JDK、Mysql、Tomcat三者制作成一个安装包,解决了客户在安装软件时的复杂配置和繁琐问题,便于管理软件版本和系统集成。具体步骤包括配置JDK环境变量和安装Mysql服务,其中使用了MySQL Server 5.5社区版和my.ini文件。安装方法为通过命令行将目录转到mysql的bin目录下,执行mysqld --install MySQL5命令。 ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • Spring源码解密之默认标签的解析方式分析
    本文分析了Spring源码解密中默认标签的解析方式。通过对命名空间的判断,区分默认命名空间和自定义命名空间,并采用不同的解析方式。其中,bean标签的解析最为复杂和重要。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • t-io 2.0.0发布-法网天眼第一版的回顾和更新说明
    本文回顾了t-io 1.x版本的工程结构和性能数据,并介绍了t-io在码云上的成绩和用户反馈。同时,还提到了@openSeLi同学发布的t-io 30W长连接并发压力测试报告。最后,详细介绍了t-io 2.0.0版本的更新内容,包括更简洁的使用方式和内置的httpsession功能。 ... [详细]
  • 闭包一直是Java社区中争论不断的话题,很多语言都支持闭包这个语言特性,闭包定义了一个依赖于外部环境的自由变量的函数,这个函数能够访问外部环境的变量。本文以JavaScript的一个闭包为例,介绍了闭包的定义和特性。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 欢乐的票圈重构之旅——RecyclerView的头尾布局增加
    项目重构的Git地址:https:github.comrazerdpFriendCircletreemain-dev项目同步更新的文集:http:www.jianshu.comno ... [详细]
author-avatar
艹丶马化腾_323
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有