参考:

java guide : https://javaguide.cn/java/concurrent/java-concurrent-questions-03.html

阿里云开发者社区:https://developer.aliyun.com/article/1180633

https://blog.csdn.net/Dongguabai/article/details/105342001?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-0-105342001-blog-124063590.235^v38^pc_relevant_sort_base2&spm=1001.2101.3001.4242.1&utm_relevant_index=3

 

ThreadLocal 介绍

ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以存储每个线程的私有数据。

如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本

Thread 类

public class Thread implements Runnable {
    //......
    //与此线程有关的ThreadLocal值。由ThreadLocal类维护
    ThreadLocal.ThreadLocalMap threadLocals = null;

    //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    //......
}

ThreadLocal 类的 set() 方法

public void set(T value) {
    //获取当前请求的线程
    Thread t = Thread.currentThread();
    //取出 Thread 类内部的 threadLocals 变量(哈希表结构)
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 将需要存储的值放入到这个哈希表中
        map.set(this, value);
    else
        createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

最终的变量是放在了当前线程的 ThreadLocalMap,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。 ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。

 

ThreadLocal 使用示例

ThreadLocalMap 类的定义在 ThreadLocal<T> 类中,是 ThreadLocal 类的一个内部类, key 是 ThreadLocal<T> 的一个对象,value 是这个 <T> 的实际值

Thread 线程类有一个字段,ThreadLocal.ThreadLocalMap threadLocals = null,是 ThreadLocal.ThreadLocalMap 类的一个实现对象,也就是每个线程对象都有一个 ThreadLocalMap 对象

ThreadLocal<T> 相当于一个维护所有线程 ThreadLocal.ThreadLocalMap threadLocals = null 这个属性,即所有线程 <T> 的取值 value 的一个工具类(所以一般声明为 static)

下面用一个例子说明这一点:

下面这段代码里,定义了两个 ThreadLocal<T> 对象

  • private static final ThreadLocal<TreeNode> treeNodeThreadLocal = new ThreadLocal<>();
  • private static final ThreadLocal<Integer> integerThreadLocal = new ThreadLocal<>().withInitial(() -> 0);

每个线程生成一个随机整数 curThreadInt

然后分别向这两个 ThreadLocal<T> 对象存放刚才生成的 随机整数 curThreadInt

import java.util.Random;

public class testThreadLocal implements Runnable {

    private static final ThreadLocal<TreeNode> treeNodeThreadLocal = new ThreadLocal<>();

    private static final ThreadLocal<Integer> integerThreadLocal = new ThreadLocal<>().withInitial(() -> 0);

    public static void main(String[] args) throws InterruptedException {
        testThreadLocal thisclass = new testThreadLocal();
        for(int i=0 ; i<10; i++){
            Thread t = new Thread(thisclass, ""+i);
            // 目的让各个线程打印初始值和 set,get 交错
            Thread.sleep(new Random().nextInt(1000));
            t.start();
        }
    }

    @Override
    public void run() {
        System.out.println("Thread Name= "+Thread.currentThread().getName()+" integerThreadLocal = "+ integerThreadLocal.get());
        Integer curThreadInt = new Random().nextInt(1000);

        // 目的让各个线程打印初始值和 set,get 交错
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        treeNodeThreadLocal.set(new TreeNode(curThreadInt, Thread.currentThread().getName()));
        integerThreadLocal.set(curThreadInt);

        System.out.println("Thread Name= "+Thread.currentThread().getName()
                +" integerThreadLocal = "+integerThreadLocal.get()
                +" treeNodeThreadLocal = "+treeNodeThreadLocal.get());
    }

    private class TreeNode {
        private Integer index;
        private String name;
        private TreeNode leftChild;
        private TreeNode rightChild;
        public TreeNode(Integer index, String name) {
            this.index = index;
            this.name = name;
        }
        @Override
        public String toString() {
            return "TreeNode{" +
                    "index=" + index +
                    ", name='" + name + '\'' +
                    '}';
        }
    }

}

输出结果如下

D:\Program\Java\jdk1.8.0_321\bin\java.exe -javaagent:D:\Program\IDEA2022\lib\idea_rt.jar=61996:D:\Program\IDEA2022\bin -Dfile.encoding=UTF-8 -classpath D:\Program\Java\jdk1.8.0_321\jre\lib\charsets.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\deploy.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\ext\access-bridge-32.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\ext\cldrdata.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\ext\dnsns.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\ext\jaccess.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\ext\jfxrt.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\ext\localedata.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\ext\nashorn.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\ext\sunec.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\ext\sunjce_provider.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\ext\sunmscapi.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\ext\sunpkcs11.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\ext\zipfs.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\javaws.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\jce.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\jfr.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\jfxswt.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\jsse.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\management-agent.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\plugin.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\resources.jar;D:\Program\Java\jdk1.8.0_321\jre\lib\rt.jar;D:\001Know\a_project\myTest\target\classes;C:\Users\Administrator\.m2\repository\com\alibaba\fastjson\1.2.83\fastjson-1.2.83.jar;C:\Users\Administrator\.m2\repository\org\freemarker\freemarker\2.3.20\freemarker-2.3.20.jar testThreadLocal
Thread Name= 0 integerThreadLocal = 0
Thread Name= 0 integerThreadLocal = 302 treeNodeThreadLocal = TreeNode{index=302, name='0'}
Thread Name= 1 integerThreadLocal = 0
Thread Name= 1 integerThreadLocal = 441 treeNodeThreadLocal = TreeNode{index=441, name='1'}
Thread Name= 2 integerThreadLocal = 0
Thread Name= 3 integerThreadLocal = 0
Thread Name= 4 integerThreadLocal = 0
Thread Name= 2 integerThreadLocal = 390 treeNodeThreadLocal = TreeNode{index=390, name='2'}
Thread Name= 3 integerThreadLocal = 941 treeNodeThreadLocal = TreeNode{index=941, name='3'}
Thread Name= 5 integerThreadLocal = 0
Thread Name= 4 integerThreadLocal = 285 treeNodeThreadLocal = TreeNode{index=285, name='4'}
Thread Name= 5 integerThreadLocal = 129 treeNodeThreadLocal = TreeNode{index=129, name='5'}
Thread Name= 6 integerThreadLocal = 0
Thread Name= 6 integerThreadLocal = 926 treeNodeThreadLocal = TreeNode{index=926, name='6'}
Thread Name= 7 integerThreadLocal = 0
Thread Name= 7 integerThreadLocal = 823 treeNodeThreadLocal = TreeNode{index=823, name='7'}
Thread Name= 8 integerThreadLocal = 0
Thread Name= 9 integerThreadLocal = 0
Thread Name= 9 integerThreadLocal = 556 treeNodeThreadLocal = TreeNode{index=556, name='9'}
Thread Name= 8 integerThreadLocal = 615 treeNodeThreadLocal = TreeNode{index=615, name='8'}

Process finished with exit code 0

treeNodeThreadLocal 和 integerThreadLocal 这两个 ThreadLocal 类的对象与线程的关系如下所示

ThreadLocal. ThreadLocalMap 内部类定义

/**
     * ThreadLocalMap is a customized hash map suitable only for
     * maintaining thread local values. No operations are exported
     * outside of the ThreadLocal class. The class is package private to
     * allow declaration of fields in class Thread.  To help deal with
     * very large and long-lived usages, the hash table entries use
     * WeakReferences for keys. However, since reference queues are not
     * used, stale entries are guaranteed to be removed only when
     * the table starts running out of space.
     */
    static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

        /**
         * 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

        /**
         * Set the resize threshold to maintain at worst a 2/3 load factor.
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

        /**
         * Increment i modulo len.
         */
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

        /**
         * Decrement i modulo len.
         */
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }

        /**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        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);
        }

        /**
         * Construct a new map including all Inheritable ThreadLocals
         * from given parent map. Called only by createInheritedMap.
         *
         * @param parentMap the map associated with parent thread.
         */
        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

        /**
         * Get the entry associated with key.  This method
         * itself handles only the fast path: a direct hit of existing
         * key. It otherwise relays to getEntryAfterMiss.  This is
         * designed to maximize performance for direct hits, in part
         * by making this method readily inlinable.
         *
         * @param  key the thread local object
         * @return the entry associated with key, or null if no such
         */
        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

        // ...... 后面的就不贴过来了

 

ThreadLocal 的内存泄露问题

ThreadLocal 内存泄漏的场景

ThreadLocal关联两个引用,在ThreadLcocal对象上是强引用,而在Entry对象上是弱引用

如果gc想回收,必须ThreadLocal对象本身不存在强引用,在将 ThreadLocal 定义为局部变量时有两种情况:

  1. 不会发生内存泄漏:线程结束,ThreadLocal 和 线程中 的 ThreadLocalMap 包括 key 和 value 一起被回收
  2. 会发生内存泄漏:ThreadLocal 对象先于线程中 ThreadLocalMap 被垃圾回收
    • 在 线程还没结束的时候将ThreadLocal对象赋值为null(或者像下图这个例子一样作为内部方法的局部变量,后内部方法返回,这意味着对象已经死亡,可以进行垃圾回收,这时ThreadLocal就不再存在强引用,而 Entry 对象 key 引用的 ThreadLocal 由于是弱引用,也会被垃圾回收掉,而Value对象还存在Entry的强引用,value对象不会被垃圾回收,这就导致只要线程不结束,那Entry的key就为null,对应的value也就无法被访问,造成了内存泄漏,只有等线程结束才会被回收。

如果产生了内存泄漏,其实线程的存活时间比较短的话,影响程度一般不会很大,但是如果是在线程池中使用,就会产生较大的影响,因为线程池中的线程由于要重复利用,核心线程不会进入终止状态,这就意味着,线程会持续存货不会消亡,导致 value 对象一直不被回收。

不过现实中一般都不会把 ThreadLocal 定义为只有一个线程能访问到的局部变量,一个线程是不需要线程隔离的,所以这样是没有意义的。而是定义成 全局变量。

ThreadLocal 设置弱引用原因

从上面的分析,我们发现内存泄漏的一部分原因是由于,Entry对象的弱引用,但是为什么设置Entry引用的ThreadLocal对象为弱引用呢,其实将他设置成弱引用也是防止内存泄漏的,看到这里,一些读者可定会很困惑,内存泄漏的原因和他有关,但是为什么他却又是防止内存泄漏呢?

其实,ThreadLocal的内存泄漏可能分两种

  1. key内存泄漏(在 key 为对 ThreadLocal 的强引用时会发生)
    这种内存泄漏我们是看不见的,为什么,就是因为将ThreadLocal使用弱引用才解决了这个问题,如果设计成强引用,这时我们在业务方法中将ThreadLocal设置为null,这时Entry对象中对ThreadLocal还是强引用,就意味着ThreadLocal不是进行垃圾回收(gc 回收的条件是不存在任何强引用),直接导致了key的内存泄漏。
  2. value内存泄漏(在 key 为对 ThreadLocal 的弱引用时会发生)
    其实value的内存泄漏就是我们前面分析的内存泄漏,为了防止他发生,所以调用remove方法,除此之外,set和get方法也有将ThreadLocal为null的Enrty删除掉的逻辑,目的也就是处理这种情况

ThreadLocal 提供的解决方法

ThreadLocal也提供了remove方法来方式来防止内存泄漏。在使用完数据后,令 ThreadLocal = null 之前,调用 remove() 清理掉就行了。我们看一下remove方法的实现

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

 

弱引用介绍:

如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。

ThreadLocal 与 线程复用

因为是作为多线程时的线程隔离,所以现实应用中一般把 ThreadLocal 定义为全局的 static,只要不显式地令 ThreadLocal 为 null,就不会发生内存泄漏。

但是即使这样,如果使用不当,还是会导致一些业务问题,因为创建和销毁线程的代价比较大,所以现在的线程一般都是通过线程池复用的。

就 java 应用来说,处理 web 请求的 tomcat 线程,rpc 的 dubbo 线程,MQ 发送端消费端处理线程, 都是线程池中的。

也就是说,如果你处理一笔业务时,向 ThreadLocal 设置了一些变量,而在业务结束后没有清除这些变量。这个线程会被之后的业务复用,之后的业务就会取到错误的值。