Java 程序员Java

ThreadLocal的源码解析以及内存泄漏的原理分析

2021-10-21  本文已影响0人  程序花生

介绍了Java中的ThreadLocal的作用、原理、源码以及应用,并且介绍了ThreadLocal的内存泄漏的原理以及解决办法。

1 ThreadLocal的概述

1.1 ThreadLocal的入门

public class ThreadLocal< T > extends Object

ThreadLocal来自JDK1.2,位于java.lang包。ThreadLocal可以提供线程内的局部变量,这种变量在线程的生命周期内起作用,ThreadLocal又叫做线程本地变量/线程本地存储。

实际上,单就ThreadLocal这个类来说,它不存储任何内容,真正存储数据的集合在每个Thread中的threadLocals变量里面,ThreadLocal中只是定义了这个集合的结构,并提供了一系列操作的方法。后面的源码分析处会讲到!

可以说,ThreadLocal只是一个工具类,一个对各个线程的threadLocals进行操作的工具而已。

ThreadLocal 的作用和目的:

  1. 用于实现线程内的数据共享,某些数据是以线程为作用域并且不同线程具有不同的数据副本时,即数据在线程之间隔离,就可以考虑用ThreadLocal。即对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。
  2. 方便同一个线程复杂逻辑下的数据传递,有些时候一个线程中的任务过于复杂,我们又需要某个数据能够贯穿整个线程的执行过程,可能涉及到不同类/函数之间数据的传递。此时使用Threadlocal存放数据,在线程内部只要通过get方法就可以获取到在该线程中存进去的数据,方便快捷。

1.2 同步和ThreadLocal

同步与ThreadLocal是解决多线程中数据访问问题的两种思路,前者是数据共享的思路,后者是数据隔离的思路。同步是一种以时间换空间的思想,ThreadLocal是一种空间换时间的思想。前者仅提供一份变量,让不同的线程排队访问,实现串行化;而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

Threadlocal并不能代替同步,注意ThreadLocal不是用来解决共享对象的多线程访问问题的。通过ThreadLocal的set()方法设置到线程的threadLocals里的是线程自己要存储的对象,其他线程不需要去访问,也是访问不到的。各个线程中的threadLocals以及里面的值都是不同的对象。Threadloocal是用来进行变量隔离,就是说ThreadLocal是针对那些不需要共享的属性!

1.3 主要API方法与使用案例

ThreadLocal类主要有四个可供调用的方法:

  1. void set(T value):保存值;
  2. T get():获取值;
  3. void remove():移除值;
  4. initialValue():返回该线程局部变量的初始值,该方法是为了让子类继承而设计的。这个方法是一个延迟调用方法,在一个线程第一次调用get()时(并且set未被调用)才执行。ThreadLocal中的默认实现是直接返回一个null。

ThreadLocal实现线程内数据共享,线程间数据隔离的案例:

public class ThreadLocalTest {
    /**
     * 全局ThreadLocal对象位于堆中,这是线程共享的,而方法栈,是每个线程私有的
     */
    static ThreadLocal<String> th = new ThreadLocal<>();

    public static void set() {
        //设置值,值为当前线程的名字
        th.set(Thread.currentThread().getName());
    }

    public static String get() {
        //获取值
        return th.get();
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("主线程中尝试获取值:" + get());
        //主线程中设置值,值为线程名字
        set();
        //主线程中尝试获取值
        System.out.println("主线程中再次尝试获取值:" + get());
        //开启一条子线程
        Thread thread = new Thread(new Th1(), "child");
        thread.start();
        //主线程等待子线程执行完毕
        thread.join();
        System.out.println("等待子线程执行完毕,主线程中再次尝试获取值:" + get());
    }

    static class Th1 implements Runnable {
        @Override
        public void run() {
            System.out.println("子线程中尝试获取值:" + get());
            //子线程中设置值,值为线程名字
            set();
            System.out.println("子线程中再次尝试获取值:" + get());
        }
    }
}

结果如下:

主线程中尝试获取值:null
主线程中再次尝试获取值:main
子线程中尝试获取值:null
子线程中再次尝试获取值:child
等待子线程执行完毕,主线程中再次尝试获取值:main

先设置值,然后获取,可以得到“main”。然后开启子线程,在子线程内部,先获取,得到null,然后设置值,再获取,得到“child”。最后在主线程中再尝试获取,得到的还是原值“main”,这说明ThreadLocal使得变量的作用范围限制在本线程中了,其他线程是无法访问到该变量的。

注意这里由于案例演示在最后并没有调用remove方法,在实际使用中应该在使用完毕之后调用remove方法,原理后面会讲!

2 ThreadLocal的原理

2.1 基本关系

ThreadLocal类中定义了一个内部类ThreadLocalMap,ThreadLocalMap是真正存放数据的容器,实际上它的底层就是一张哈希表。

每个Thread线程内部都定义有一个ThreadLocal.ThreadLocalMap类型的threadLocals变量,这样,线程之间的ThreadLocalMap互不干扰。threadLocals变量持有的ThreadLocalMap在ThreadLocal调用set或者get方法时才会初始化。

ThreadLocal还提供相关方法,负责向当前线程的ThreadLocalMap变量获取和设置线程的变量值,相当于一个工具类。

当在某个线程的方法中使用ThreadLocal设置值的时候,就会将该ThreadLocal对象添加到该线程内部的ThreadLocalMap中,其中键就是该ThreadLocal对象,值可以是任意类型任意值。当在某个线程的方法中使用ThreadLocal获取值的时候,会以该ThreadLocal对象为键,在该线程的ThreadLocalMap中获取对应的值。

ThreadLocal中定义了ThreadLocalMap的结构,并提供操作的方法:

    public class ThreadLocal<T> {
        //……
        static class ThreadLocalMap {
            //……
        }

        /**
         * ThreadLocal的构造器,里面什么都没有
         * 创建ThreadLocal时,没有初始化ThreadLocalMap,在set、get方法中还可能初始化!
         */
        public ThreadLocal() {
        }
    }

每个thread对象都持有一个ThreadLocalMap类型的引用变量,用于存放线程本地变量。key为ThreadLocal对象,value为要存储的数据。

public class Thread implements Runnable {
    /*与此线程相关的线程本地值。此ThreadLocalMap定义在ThreadLocal类中,使用在Thread类中*/
    ThreadLocal.ThreadLocalMap threadLocals = null;
    //………………
}

下面是Thread、threadlocalMap、ThreadLocal的关系图:

2.2 基本结构

ThreadLocal中定义了ThreadLocalMap的结构。

ThreadLocalMap也是一张key-value类型的哈希表,但是ThreadLocalMap并没有实现Map接口,它内部具有一个Entry类型的table数组用于存放节点。Entry节点用于存放key、value数据,并且继承了WeakReference。

通过对该 ThreadLocal 对象进行哈希运算,可以得到该 ThreadLocal 对象在 Entry 数组中的桶位,从而找到唯一的 Entry。**如果发生了哈希冲突,那么与 HashMap 和 Hashtable 采用的“链地址法”不同,ThreadLocalMap 采用“线性探测法”解决哈希冲突,采用该方法的原因是实现很简单,占用更小的空间,并且一般来说一个ThreadLocalMap并不会存放很多数据!
在创建ThreadLocalMap对象的同时即初始化16个长度的内部table数组,扩容阈值为len * 2 / 3,扩容增量为增加原容量的1倍

在没有使用ThreadLocal设置、获取值时,线程中的ThreadLocalMap对象一直为null。

/**
 * ThreadLocal的内部类ThreadLocalMap
 */
static class ThreadLocalMap {

    /**
     * table数组的初始化容量,
     */
    private static final int INITIAL_CAPACITY = 16;
    //存放数据的数组,在创建ThreadLocalMap对象时将会初始化该数组,大小必须是2^N次方
    private Entry[] table;
    //扩容阈值,为len * 2 / 3
    private int threshold;

    /**
     * 内部节点对象,貌似没找到“key”字段在哪里,实际上存放k的属性位于其父类WeakReference的父类Reference中,名为referent,即属性复用
     * 插入数据时,通过对key(threadLocal对象)的hash计算,来找出Entry应该存放的table数组的桶位,
     * 不过可能造成hash冲突,它采用线性探测法解决冲突,因此需要线性向后查找。
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        //存放值
        Object value;
        //构造器
        Entry(ThreadLocal<?> k, Object v) {
            //调用父类的构造器,传入key,这里k被包装成为弱引用
            //实际上存放k的属性位于其父类WeakReference的父类Reference中,名为referent,即属性复用
            super(k);
            value = v;
        }
    }
}

2.3 set方法

set方法是由ThreadLocal提供的,用于存放数据,大概步骤如下:

  1. 获取当前线程的成员变量threadLocals;
  2. 如果threadLocals不等于null,则调用set方法存放数据,方法结束;
  3. 否则,调用createMap方法初始化threadLocals,然后存放数据,方法结束。
/**
 * ThreadLocal中的方法,开放给外部调用的存放数据的方法
 *
 * @param value 需要存放的数据
 */
public void set(T value) {
    //注意,这里首先获取当前线程t
    Thread t = Thread.currentThread();
    //1.1 然后通过getMap方法,传入t,获取当前t线程的threadLocals
    ThreadLocalMap map = getMap(t);
    //如果map存在,则存放数据
    if (map != null)
        //this代指当前ThreadLocal对象,value表示值
        map.set(this, value);
    else
        //如果不存在,则构建属于当前线程的ThreadLocalMap并存放数据
        createMap(t, value);
}

/**
 * 1.1 ThreadLocal中的方法,获取指定线程的threadLocals
 *
 * @param t 指定线程
 * @return t的threadLocals
 */
ThreadLocalMap getMap(Thread t) {
    //t代表当前线程,获取该线程的threadLocals属性,该属性就是一个ThreadLocalMap,默认为null
    return t.threadLocals;
}

/**
 * 1.2 ThreadLocal中的方法,用于构建ThreadLocalMap对象并赋值
 *
 * @param t          当前线程
 * @param firstValue 要存入的值
 */
void createMap(Thread t, T firstValue) {
    //该方法是ThreadLocal中的方法,this代指当前ThreadLocal对象
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

/**
 * 位于ThreadLocalMap中的构造器,用于创建新的ThreadLocalMap对象
 *
 * @param firstKey   key
 * @param firstValue value
 */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    //创建table数组,初始容量为INITIAL_CAPACITY,即16
    table = new Entry[INITIAL_CAPACITY];
    //寻找数组桶位,通过ThreadLocal对象的threadLocalHashCode属性 & 15
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    //该位置存放元素,由于是刚创建对象,因此不存在哈希冲突的情况,直接存储就行了
    //构造器在“基本结构”部分分析过,key最终被包装成弱引用。
    table[i] = new Entry(firstKey, firstValue);
    //size设置为1
    size = 1;
    //setThreshold方法设置扩容阀值
    setThreshold(INITIAL_CAPACITY);
}

/**
 * ThreadLocalMap中的方法,设置扩容阈值
 *
 * @param len 数组长度
 */
private void setThreshold(int len) {
    //数组长度的2/3
    threshold = len * 2 / 3;
}
2.3.1 内部set方法

上面的set方法中,如果当前t线程的threadLocals不为null,那么又调用了另一个私有的set方法存放数据。该方法是ThreadLocal的核心方法之一,并且比较复杂,大概具有如下步骤:

  1. 通过哈希算法计算出当前key存放的桶位i,并获取i的元素e。
  2. 如果e不为空,说明发生哈希冲突,使用线性探测法替换或者存放数据:
    1. 如果e的key和指定key相等(使用==比较),那么替换value,方法结束;
    2. 否则,如果e的key等于null,那说明是无效数据。调用replaceStaleEntry从该索引开始清理无效数据,并且存放新数据,在replaceStaleEntry过程中:
      1. 如果找到了key相等的entry,则它放到无效桶位中,value置为新值,方法结束。
      2. 如果没找到key相等的entry,直接在无效slot原地放entry,方法结束。
      3. 调用到了replaceStaleEntry方法,那就肯定能将新数据存入ThreadLocalMap中,并且不再执行后续步骤。
    3. 否则,nextIndex方法获取下一个索引并赋值给i,如果该位置的节点e为null,则结束循环,否则进行下一次循环;
  3. 走到这一步,说明没有替换value,也没有没有进行无效数据清理,而是找到了一个空桶位i,直接在该位置插入新entry,此时肯定保证最初始的i和现在的之间的位置都是存在有效节点的;
  4. 存放元素完毕之后,再调用cleanSomeSlots做一次部分无效节点清理,如果没清理出去key(返回false)并且当前table大小 大于等于 阈值,则调用rehash方法;
  5. rehash方法中会调用一次全表扫描清理的方法即expungeStaleEntries()方法。如果expungeStaleEntries完毕之后table大小还是大于等于(threshold – threshold / 4),则调用resize方法进行扩容;
  6. resize方法将扩容两倍,同时完成节点的转移。

ThreadLocalMap使用==比较key是否相同。ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1(线性探测),寻找下一个相邻的位置。当向前寻找到数组头部或者向后寻找到数组尾部的时候,下一个位置就是数组尾部或者数组头部,即循环查找。

/**
 * 位于ThreadLocalMap内的set方法,用于存放数据。
 *
 * @param key   ThreadLocal对象
 * @param value 值
 */
private void set(ThreadLocal<?> key, Object value) {
    //tab保存数组引用
    Entry[] tab = table;
    //len保存数组的度
    int len = tab.length;
    /*1 哈希算法计算桶位   通过ThreadLocal的threadLocalHashCode属性计算出该key(ThreadLocal对象)对应的数组桶位i*/
    int i = key.threadLocalHashCode & (len - 1);
    /*
     *  2 使用线性探测法存放元素,可能进行垃圾清理
     * 获取i索引位置的Entry e,如果e不为null,说明发生了哈希冲突,下面开始解决:
     * 判断两个key是否相等,即是否需要进行value替换,如果相等,则替换value,解决完毕,方法返回;
     * 否则,判断获取的e的key是否为null,如果获取的ThreadLocal为null,这说明该WeakReference(弱引用)被回收了(因为Entry继承了WeakReference),
     * 说明ThreadLocal肯定在外部没有强引用了,这个Entry变成了垃圾,调用replaceStaleEntry方法擦除该位置或者其他的无效的Entry,重新赋值,解决完毕,方法返回,这是为了防止内存泄漏
     * 否则判断该位置i是否为null,即没有节点,如果为null,则在该位置新建节点并插入,解决完毕。
     * 否则,i = nextIndex(i, len),尝试下一次循环。
     * 如果循环完毕,方法还没结束,那说明没找到key相等的节点和key==null的节点,但是找到了下一个节点为null的桶位,记录此时索引值i,将会在该位置插入新节点。
     *
     * 这就是ThreadLocalMap解决哈希冲突的办法,即开放定址法——线性探测:当冲突时,向下查找下一个节点为null的位置存放新节点
     * nextIndex()方法用于循环数组索引,即如果初始i为15,长度为16,那么nextIndex将返回0,如果初始i为1,长度为16,那么nextIndex将返回2。
     * 这样的做法有利于利用起始索引前面的空间
     * */
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        /*获取该Entry的key,即原来的ThreadLocal对象,这是其父类Reference的方法*/
        ThreadLocal<?> k = e.get();
        /*如果获取的ThreadLocal和要存的ThreadLocal是同一个对象,那么就替换值,方法结束
         * 这里能够看出,判断key相等的条件是两个对象使用==比较返回true
         * */
        if (k == key) {
            e.value = value;
            return;
        }
        /*
         *如果获取的ThreadLocal为null,这说明该WeakReference(弱引用)被回收了(因为Entry继承了WeakReference),
         * 说明ThreadLocal肯定在外部没有强引用了,这个Entry变成了垃圾,擦除该位置的Entry,重新赋值并结束方法,这是为了防止内存泄漏*/
        if (k == null) {
            /*
             * 从该位置开始,继续寻找key,并且会尽可能清理其他无效slot
             * 在replaceStaleEntry过程中,如果找到了key,则做一个swap把它放到那个无效slot中,value置为新值
             * 在replaceStaleEntry过程中,没有找到key,直接在该无效slot原地放entry
             * */
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    /*
     * 执行到这一步方法还没有返回,说明i位置没有节点,此时e等于null,直接在该位置插入新的Entry
     * 此时肯定保证最初始的i和现在的之间的位置是存在节点的!
     * */
    tab[i] = new Entry(key, value);
    //size自增1,使用sz记录
    int sz = ++size;
    /* 3 尝试清理垃圾,然后判断是否需要扩容,如果需要那就扩容
     * 存放完毕元素之后,再调用cleanSomeSlots做一次垃圾清理,如果没清理出去key(返回false)
     * 并且当前table大小大于等于阈值,则调用rehash方法
     * rehash方法中会调用一次全量清理slot方法也即expungeStaleEntries()方法
     * 如果expungeStaleEntries完毕之后table大小还是大于等于(threshold – threshold / 4),则调用resize方法进行扩容
     * resize方法将扩容两倍,同时完成节点的转移
     * */
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

/**
 * 在length的索引范围内获取i的下一个索引,循环
 */
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

/**
 * 在length的索引范围内获取i的上一个索引,循环
 */
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

private void rehash() {
    expungeStaleEntries();
    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        resize();
}
2.3.2 哈希算法

在set方法中,我们可以找到ThreadLocalMap的哈希算法为:

int i = key.threadLocalHashCode & (len - 1);

由于len长度一定是2的幂次方,因此上面的位运算可以转换为key.threadLocalHashCode% len,所以说ThreadLocalMap的哈希算法也是一种取模(求余)算法,因为余数一定会比除数小,那么计算出来的桶位肯定是位于[0, len-1]之间了,刚好在底层数组的索引范围内,还是比较简单的。

这里的key我们知道是ThreadLocal对象,这个threadLocalHashCode属性看名字猜测就是该对象的哈希值了,那么这个值是通过hashCode方法得到的吗?实际上,threadLocalHashCode这个属性的得来非常的有趣,我们必须要去ThreadLocal源码中去看看!

public class ThreadLocal<T> {
    /**
     * 下一个hashCode
     * 注意:这是个静态属性,那么只有在ThreadLocal的类第一次被加载进行类初始化的时候会被初始化,明显,初始化时为0。
     */
    private static AtomicInteger nextHashCode = new AtomicInteger();

    /**
     * threadlocal对象的hashcode,并非通过HashCode方法得到,他有自己的计算规则
     * 可以看到,它是调用nextHashCode()方法的返回值得来的
     */
    private final int threadLocalHashCode = nextHashCode();

    /**
     * 每个threadLocal对象通过该方法获取自己的hashcode
     */
    private static int nextHashCode() {
        //内部使用nextHashCode对象的getAndAdd方法
        //该方法首先返回当前的值,然后使得当前值的值加上指定的值,这里是HASH_INCREMENT
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

    /**
     * 哈希增量,顾名思义,就是哈希值的增量
     */
    private static final int HASH_INCREMENT = 0x61c88647;
}

结合上面的几个属性和方法,我们终于明白:

在第一次创建ThreadLocal实例时,会加载ThreadLocal类,此时nextHashCode初始化值为0,然后是该对象threadLocalHashCode属性的初始化,在创建该类对象完毕之后,会自动调用nextHashCode方法,将此时nextHashCode的值作为自己的hashCode并且nextHashCode对象的值增加HASH_INCREMENT,明显是作为下一个ThreadLocal实例的hashCode值。

即,每一个ThreadLocal实例使用创建该实例时的nextHashCode值作为自己的hashCode,然后将nextHashCode值增加HASH_INCREMENT,作为下一个ThreadLocal实例的hashCode。

0x61c88647是十六进制的数,转换为十进制就是1640531527,实际上这个哈希增量的值的选取和斐波那契散列法、黄金比例有关

2.4 get方法

对于不同的线程,每次获取变量值时,是从本线程内部的threadLocals中获取的,别的线程并不能获取到当前线程的值,形成了变量的隔离,互不干扰。大概步骤如下:

  1. 获取当前线程的成员变量threadLocals
  2. 如果threadLocals非空,调用getEntry方法尝试查找并返回节点e:
    1. 如果e不为null,说明找到了,那爱么返回e的value,方法结束
    2. 如果e为null,说明没找到,方法继续。

到这一步,说明可能是threadLocals为空,或者没找到e。那么调用setInitialValue方法,以当前ThreadLocal对象为key设置一个entry,并返回value。

/**
 * ThreadLocal中的get方法,开放给外部调用
 *
 * @return 当前线程的当前ThreadLocal对象存入的值
 */
public T get() {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程的threadLocals对象
    ThreadLocalMap map = getMap(t);
    //如果map不为null,即表示已经初始化过
    if (map != null) {
        //从map获取对应的Entry节点,传入this代表当前的ThreadLocal对象
        ThreadLocalMap.Entry e = map.getEntry(this);
        //如果e不为null
        if (e != null) {
            //获取并返回值
            T result = (T) e.value;
            return result;
        }
    }
    //否则,如果map为null,或者e为null
    //那么返回null或者自定义的初始值
    return setInitialValue();
}
2.4.1 getEntry方法

ThreadLocalMap内部的方法,根据key,尝试获取对应的Entry节点。 大概步骤如下:

  1. 根据key计算出桶位;
  2. 获取该桶位节点e,如果e不为null并且e的key和指定key相等(使用==比较),那么返回e,方法结束;
  3. 否则,调用getEntryAfterMiss方法进行一个步长的线性探测查找,查找过程中每碰到无效的节点,调用expungeStaleEntry进行清理;如果找到了则返回找到的entry;没有找到(探测到了空的桶位),则返回null。
/**
 * ThreadLocalMap内部的方法,根据key,获取对应的Entry节点
 *
 * @param key key
 * @return Entry节点,没找到就返回null
 */
private Entry getEntry(ThreadLocal<?> key) {
    //根据key计算桶位
    int i = key.threadLocalHashCode & (table.length - 1);
    //获取Entry节点e
    Entry e = table[i];
    /*如果e不为nul并且并且e内部key等于当前key(ThreadLocal对象)*/
    //可以看到key相等是使用==直接比较的
    if (e != null && e.get() == key)
        //则返回e
        return e;
    else
        /*否则使用线性探测查找
        线性探测查找过程中每碰到无效slot,调用expungeStaleEntry进行清理;如果找到了则返回entry;没有找到,返回null*/
        return getEntryAfterMiss(key, i, e);
}
2.4.2 setInitialValue方法

ThreadLocal的方法,用于设置并返回初始值,在get方法没有找key对应的节点时,会调用该方法!

大概有如下几步:

  1. 获取initialValue方法的返回值,作为新节点的value;
  2. 获取当前线程的ThreadLocalMap,判断是否为null;
  3. 如果不为null,则以当前ThreadLocal对象为key,存放value,方法结束;
  4. 如果为null,则初始化此线程的ThreadLocalMap,并以当前ThreadLocal对象为key,存放value,方法结束。
/**
 * ThreadLocal的方法,设置并返回初始值
 *
 * @return 返回null或者通过initialValue方法用户自定义的初始值
 */
private T setInitialValue() {
    //返回null或者用户重写该方法时自定义的返回值
    T value = initialValue();
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程的map
    ThreadLocal.ThreadLocalMap map = getMap(t);
    //如果map不为null
    if (map != null)
        //尝试添加节点,以当前ThreadLocal对象为key,以null或者自定义的初始值为value
        map.set(this, value);
    else
        //否则初始化map并设置值
        createMap(t, value);
    //返回value,null或者自定义的初始值
    return value;
}
2.4.2.1 initialValue方法

当get方法没有找到数据时,会调用setInitialValue方法,该方法中会调用initialValue方法,将默认返回null,用户也可以重写该方法,用于返回指定的值,相当于默认初始值。

setInitialValue将会以当前ThreadLocal对象为key,initialValue的返回值为value,存放一个节点,同时返回value的值。

/**
 * ThreadLocal的方法,默认返回空
 *
 * @return 默认返回null, 用户可以重写该方法返回自定义的初始值
 */
protected T initialValue() {
    return null;
}
2.4.2.2 默认初始值案例
public class ThreadLocalInitialValue {
    public static void main(String[] args) {
        //th1覆写了initialValue方法
        ThreadLocal th1 = new ThreadLocal() {
            @Override
            protected Object initialValue() {
                return 11;
            }
        };
        //th2没有覆写了initialValue方法
        ThreadLocal th2 = new ThreadLocal();
        //由于并没有调用set方法设置数据,那么两个ThreadLocal的get方法都将不能找到存放的数据
        //此时th1将返回默认初始值,并设置key:th1 value:11
        System.out.println("th1初始值:" + th1.get());
        //th2将返回null,并设置key:th2 value:null
        System.out.println("th2初始值:" + th2.get());
    }
}

2.5 ThreadLocal的内存泄露

2.5.1 内存泄漏的原理

首先是基础知识,关于Java中的引用的介绍:Java中强、软、弱、虚四种对象引用的详解和案例演示

根据上面的源码,我们知道在存放新结点时在Entry结点的构造器中,并不是直接使用ThreadLocal对象作为key的,而是使用由ThreadLocal对象包装成的弱引用对象作为Key的,key被弱引用的字段关联,获取key是也是从弱引用字段中获取的。

为什么使用弱引用包装的ThreadLocal对象作为key? 因为如果某个entry直接使使用一个普通属性和ThreadLocal对象关联,即key是强引用。那么当最外面ThreadLocal对象的全局变量引用置空时,由于在ThreadLocalMap中存在key对这个ThreadLocal对象的强引用,那么这个ThreadLocal对象并不会被回收,但此时我们已经无法访问、利用这个对象,造成了key的内存泄漏。

因此,ThreadLocal对象被包装为弱引用作为key。这样,当外部的ThreadLocal对象的强引用被清除时,由于在ThreadLocalMap中存储的是弱引用key,这个ThreadLocal对象只被弱引用对相关联,因此它就是一个弱引用对象,那么下一次GC时这个弱引用ThreadLocal对象可以自动被清除了。

但是,此时仍然会造成内存泄漏,不过此时是value或者说Entry的内存泄漏。

我们知道value是强引用。这就导致了一个问题,如果这个弱引用key被回收而变成null时,如果之前调用ThreadLocal方法设置值的线程一直持续运行,那么它的ThreadLocalMap也一直存在,那么内部的entry结点也一直存在,那么value肯定还存在,但是此时却不能通过key访问到了(因为key被回收变成null了),此时还是发生了内存泄露。

所以说,最保险的办法是移除无效的Entry。

2.5.2 如何避免内存泄漏

我们在set和get方法的源码中能够看到,当遍历的entry的key为null时,此时将清除该entry,value置空,这样就可以解决部分内存泄漏问题。但这并不是绝对的,可能并没有遍历到key为null的entry时set、get方法就因为插入、获取成功而返回了,因此在set、get方法中,只会尝试将遍历的到无效数据清除,并且这种方式是一种被动的清除,不能即时清除无效数据。

ThreadLocal还有一个remove方法,该方法可以将此ThreadLocal对象对应的entry清除。实际上,在对ThreadLocal的数据使用完毕之后,从逻辑上来说此时的entry就是无效的数据了,因此主动调用一次remove方法,将该entry移除。这样我们对使用完毕的entry进行手动清除,从根本上杜绝了内存泄漏问题。

所以养成良好的编程习惯十分重要,使用完ThreadLocal的数据之后,一定要记得调用一次remove方法。

3 总结与应用

总结:

  1. 每个ThreadLocal由于实现线程本地存储,但是只能保存一个本地数据,如果想要一个线程能够保存多个数据,就需要创建多个ThreadLocal。
  2. ThreadLocalMap的key键为ThreadLocal包装成的弱引用,value为设置的值。ThreadLocal会有内存泄漏的风险,因此使用完毕必须手动调用remove清除!

应用:

  1. 使用ThreadLocal的典型场景正如上面的数据库连接管理,线程会话管理等场景,只适用于独立变量副本的情况,如果变量为全局共享的,则不适用在高并发下使用。
  2. Spring MVC对于每一个请求线程的Request对象使用ThreadLocal属性封装到RequestContextHolder中,这样每条线程都能访问到自己的Request。
  3. Spring 声明式事务管理中,每一个事务的信息也是使用ThreadLocal属性封装到TransactionSynchronizationManager中的,以此实现不同线程的事务存储和隔离,以及事务的挂起和恢复。
  4. 原始的JDBC方式的时候可以使用ThreadLocal类来管理事务!

作者:刘Java
链接:https://juejin.cn/post/7021355637812494366
来源:稀土掘金

上一篇下一篇

猜你喜欢

热点阅读