• ThreadLocal类详解


    ThreadLocal类注释翻译

    打开JDK中ThreadLocal类源码,翻译类上注释如下(提取重点部分):

    每个访问ThreadLocal实例对象的线程都有其自己的关于ThreadLocal对象的变量副本(通过get和set方法),只要线程存活而且ThreadLocal对象也存活,则线程都保留着与ThreadLocal对象的变量副本的隐式引用。线程停止后,其对应的threadlocal变量副本会被垃圾回收。

    上面的翻译其实就表达了一个意思,即每个线程有自己的ThreadLocal对象,且Thread对象对ThreadLocal对象进行了隐式引用。如何进行的隐式引用呢?继续往下研究。

    Entry解析

    Entry是ThreadLocalMap类的一个内部类,其类注释如下:

    该类对其key值ThreadLocal对象进行了弱引用,空键(即entry.get()==null)意味着该键不再被引用,因此可以从表中删除该项。在接下来的代码中,这些条目被称为“过时条目”。

    看Entry实现:

     static class Entry extends WeakReference<ThreadLocal<?>> {
                /** The value associated with this ThreadLocal. */
                Object value;
    
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    Entry持有了ThreadLocal的弱引用。即Entry对象没有被GC回收,但是key值ThreadLocal对象可以被GC回收,此时这个Entry对象的key就是null,称为"过时条目"。

    ThreadLocalMap解析

    ThreadLocalMap是一个定制的哈希映射,仅适用于维护线程本地值。ThreadLocal没有对外获取ThreadLocalMap对象的操作。ThreadLocalMap类是私有的,允许在Thread类中声明字段。为了处理非常大和长期的使用,hash entries 对key值使用弱引用。由于使用了弱引用,所以只有当entries空间满后,才会删除过时的entries。

    上面是ThreadLocalMap类注释翻译。从上面翻译可以看出,ThreadLocal中对Thread本地存值,其实都是存到了ThreadLocalMap中。
    在Thread类中,声明了ThreadLocalMap的变量,阅读Thread类源码,可知变量为:

    /* ThreadLocal values pertaining to this thread. This map is maintained
         * by the ThreadLocal class. */
        ThreadLocal.ThreadLocalMap threadLocals = null;
    
    • 1
    • 2
    • 3

    由注释可知,这个成员变量由ThreadLocal类维护。即threadLocals变量的值,在ThreadLocal类中赋予。

    下面看ThreadLocalMap中table属性:

     private Entry[] table;
    
    • 1

    table属性存储了本地线程中key-value键值对数组,因为一个线程可以存放多个key-value,所以是数组形式。上面提到,Entry的key是ThreadLocal对象,所以,在Thread本地变量中,key就是ThreadLocal对象,value就是我们要存的值,而这个key-value对象又存放在ThreadLocalMap中的Entry对象中。在Thread类中,有ThreadLocalMap对象的属性,这样就可以获取到任意存放的本地变量。这样这个功能的逻辑就形成了闭环。

    但是jdk在设计这个功能时有些特殊,甚至说有些别扭。因为我们在给一个线程对象存取本地变量时,都是通过ThreadLocal对象的get()和set()方法来进行操作的。而我们上面又提到,ThreadLocal是线程对象本地变量Map中的一个key。用key来操作Map,把自己当成key存到Map中,这个操作是不是很另类呢?

    ThreadLocal类方法

    initialValue()方法

     protected T initialValue() {
            return null;
        }
    
    • 1
    • 2
    • 3

    初始化值。用于没有给ThreadLocal对象调用set()方法赋值,而直接调用get()方法取值时进行返回。即创建了ThreadLocal对象后,没有给其set()值,则调用get()时返回initialValue()方法的值。这个方法一般在创建ThreadLocal时通过内部类方式进行实现。

    get()方法

    public T get() {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;
                    return result;
                }
            }
            return setInitialValue();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    先看第一行,获取到当前线程,即谁调用ThreadLocal对象的get()方法,就获取到谁的线程,
    第二行调用getMap()方法,参数是当前线程,看getMap()方法源码:

    ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }
    
    • 1
    • 2
    • 3

    返回了当前线程的threadLocals属性,这个属性我们上面分析过,就是当前线程的ThreadLocalMap,而这个Map中存放了当前线程的本地变量。

    然后,调用ThreadLocalMap的getEntry()方法,参数是this,即当前的ThreadLocal对象,以此为key,找对应value。

    总体流程就是: ThreadLocal作为key,它的get()方法首先获取到当前线程,然后再获取到当前线程的本地变量Map,即ThreadLocalMap,然后在把ThreadLocal自己作为key,传入ThreadLocalMap的getEntry()方法中,获取对应的value。这个设计别扭吧。

    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);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    跟get()方法逻辑类似,获取到当前线程,然后获取到当前线程的ThreadLocalMap成员属性,然后调用Map的set方法,key就是ThreadLocal对象本身,value是参数设置的值。set()进去的key和value最终加入到Entry[] table中去。
    在这里插入图片描述

    关于ThreadLocal内存泄漏问题

    Thread类里引用了ThreadLocalMap对象,ThreadLocalMap中Entry对象对ThreadLocal对象进行了弱引用。所以自始至终,Thread对象和ThreadLocal对象都没有发生直接关系。所以,ThreadLocal对象在出栈后,会被GC。此时,Entry中key为null。但是value还是有值的。所以,这个Entry就在内存中,无法获取了。这就是内存溢出问题。示意图如下:
    在这里插入图片描述

    那为什么Entry要设置成对ThreadLocal的弱引用呢?为何不设置成强引用呢?因为如果设置成弱引用,有可能发生内存溢出,如果设置成强引用,Entry[]不被GC,ThreadLocal就不会GC。假如ThreadLocal没用了,也不会被回收,肯定造成了内存溢出。
    采用弱引用后,ThreadLocal可以被回收了,就进行回收,虽然有造成内存溢出可能,但是不是一定会造成内存溢出。而且在ThreadLocal对象的set()和get()方法中,会检查当前Thread的ThreadLocalMap中是否有key为null的Entry清除掉。
    想要避免ThreadLocal内存溢出问题,就在程序中及时调用remove()方法,及时删除掉不用的Entry对象,这样就不会有内存溢出问题了。

  • 相关阅读:
    Python与Appium实现手机APP自动化测试的示例代码
    Django模型继承之多表继承
    Spring Security配置
    MySqL速成教程笔记系列八
    ssssssssssssssssssssss
    机器学习---决策树分类代码
    SpringBoot文件上传和下载
    6.1 线性空间
    MySQL的主从复制
    南京大学计院夏令营历年机试笔试题汇总(刷题版)
  • 原文地址:https://blog.csdn.net/qq1309664161/article/details/127606213