Java中ThreadLocal说明 使用线程内变量,完成后需调用remove()方法将其移除,即使异常也记得remove()回收,创建ThreadLocal线程变量 public static ThreadLocal<String> threadLocal = new ThreadLocal<>();

发布时间 2023-11-14 08:49:20作者: sunny123456

Java中ThreadLocal说明,完成后需调用remove()方法将其移除,即使异常也记得remove()回收,创建ThreadLocal线程变量 public static ThreadLocal threadLocal = new ThreadLocal<>();

1、ThreadLocal是什么

  • ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。
    这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
    ——《Java并发编程艺术》
  • 如图:
    threadlocal.png
  • ThreadLocal,可以拆成Thread+Local•Thread—线程;local—本地的,局域的。•拼在一起就是线程局域的。线程私有的,ThreadLocal类顾名思义可以理解为线程本地变量。
    定义了一个ThreadLocal,每个线程往这个ThreadLocal中读写是线程隔离,互相之间不会影响的

2、ThreadLocal怎么用

package test;
public class ThreadLocalTest {
    static ThreadLocal<String> localVar = new ThreadLocal<>();
    static void print(String str) {
        //打印当前线程中本地内存中本地变量的值
        System.out.println(str + " :" + localVar.get());
        //清除本地内存中的本地变量
        localVar.remove();
    }
    public static void main(String[] args) {
        Thread t1  = new Thread(new Runnable() {
            @Override
            public void run() {
                //设置线程1中本地变量的值
                localVar.set("localVar1");
                //调用打印方法
                print("thread1");
                //打印本地变量
                System.out.println("after remove : " + localVar.get());
            }
        });
        Thread t2  = new Thread(new Runnable() {
            @Override
            public void run() {
                //设置线程1中本地变量的值
                localVar.set("localVar2");
                //调用打印方法
                print("thread2");
                //打印本地变量
                System.out.println("after remove : " + localVar.get());
            }
        });
        t1.start();
        t2.start();
    }
}
  • 输出
    1368768201906132249453851072836449.png

3、ThreadLocal的实现原理

  • Thread类中有两个变量threadLocals和inheritableThreadLocals,二者都是ThreadLocal内部类ThreadLocalMap类型的变量,我们通过查看内部内ThreadLocalMap可以发现实际上它类似于一个HashMap。在默认情况下,每个线程中的这两个变量都为null,只有当线程第一次调用ThreadLocal的set或者get方法的时候才会创建他们(后面我们会查看这两个方法的源码)。
  • 每个线程的本地变量不是存放在ThreadLocal实例中,而是放在调用线程的ThreadLocals变量里面。也就是说,ThreadLocal类型的本地变量是存放在具体的线程空间上,其本身相当于一个装载本地变量的工具壳,通过set方法将value添加到调用线程的threadLocals中,当调用线程调用get方法时候能够从它的threadLocals中取出变量。如果调用线程一直不终止,那么这个本地变量将会一直存放在他的threadLocals中,所以不使用本地变量的时候需要调用remove方法将threadLocals中删除不用的本地变量
  • 流程图
    136876820190614000329689872917045.png
  • set 方法
public void set(T value) {
    //(1)获取当前线程(调用者线程)
    Thread t = Thread.currentThread();
    //(2)以当前线程作为key值,去查找对应的线程变量,找到对应的map
    ThreadLocalMap map = getMap(t);
    //(3)如果map不为null,就直接添加本地变量,key为当前定义的ThreadLocal变量的this引用,值为添加的本地变量值
    if (map != null)
        map.set(this, value);
    //(4)如果map为null,说明首次添加,需要首先创建出对应的map
    else
        createMap(t, value);
}
  • get 方法
public T get() {
    //(1)获取当前线程
    Thread t = Thread.currentThread();
    //(2)获取当前线程的threadLocals变量
    ThreadLocalMap map = getMap(t);
    //(3)如果threadLocals变量不为null,就可以在map中查找到本地变量的值
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //(4)执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量
    return setInitialValue();
}
private T setInitialValue() {
    //protected T initialValue() {return null;}
    T value = initialValue();
    //获取当前线程
    Thread t = Thread.currentThread();
    //以当前线程作为key值,去查找对应的线程变量,找到对应的map
    ThreadLocalMap map = getMap(t);
    //如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
    if (map != null)
        map.set(this, value);
    //如果map为null,说明首次添加,需要首先创建出对应的map
    else
        createMap(t, value);
    return value;
}
  • remove方法的实现
public void remove() {
    //获取当前线程绑定的threadLocals
     ThreadLocalMap m = getMap(Thread.currentThread());
     //如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量
     if (m != null)
         m.remove(this);
 }
  • ThreadLocal不支持继承性
  • 1、同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。(threadLocals中为当前调用线程对应的本地变量,所以二者自然是不能共享的)

    package test;
    public class ThreadLocalTest2 {
        //(1)创建ThreadLocal变量
        public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
        public static void main(String[] args) {
            //在main线程中添加main线程的本地变量
            threadLocal.set("mainVal");
            //新创建一个子线程
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("子线程中的本地变量值:"+threadLocal.get());
                }
            });
            thread.start();
            //输出main线程中的本地变量值
            System.out.println("mainx线程中的本地变量值:"+threadLocal.get());
        }
    }
    

    4、从ThreadLocalMap看ThreadLocal使用不当的内存泄漏问题

    • 首先我们先看看ThreadLocalMap的类图,在前面的介绍中,我们知道ThreadLocal只是一个工具类,他为用户提供get、set、remove接口操作实际存放本地变量的threadLocals(调用线程的成员变量),也知道threadLocals是一个ThreadLocalMap类型的变量,下面我们来看看ThreadLocalMap这个类。在此之前,我们回忆一下Java中的四种引用类型,相关GC只是参考前面系列的文章(JVM相关)

    • ①强引用:Java中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被GC。

    • ②软引用:简言之,如果一个对象具有弱引用,在JVM发生OOM之前(即内存充足够使用),是不会GC这个对象的;只有到JVM内存不足的时候才会GC掉这个对象。软引用和一个引用队列联合使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列中

    • ③弱引用(这里讨论ThreadLocalMap中的Entry类的重点):如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器GC掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用的对象呗回收掉之后,再调用get方法就会返回null

    • ④虚引用:虚引用是所有引用中最弱的一种引用,其存在就是为了将关联虚引用的对象在被GC掉之后收到一个通知。(不能通过get方法获得其指向的对象)

    20220823.png

    • 分析ThreadLocalMap内部实现

    1、上面我们知道ThreadLocalMap内部实际上是一个Entry数组,我们先看看Entry的这个内部类

    /**
     * 是继承自WeakReference的一个类,该类中实际存放的key是
     * 指向ThreadLocal的弱引用和与之对应的value值(该value值
     * 就是通过ThreadLocal的set方法传递过来的值)
     * 由于是弱引用,当get方法返回null的时候意味着坑能引用
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** value就是和ThreadLocal绑定的 */
        Object value;
        //k:ThreadLocal的引用,被传递给WeakReference的构造方法
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    //WeakReference构造方法(public class WeakReference<T> extends Reference<T> )
    public WeakReference(T referent) {
        super(referent); //referent:ThreadLocal的引用
    }
    //Reference构造方法
    Reference(T referent) {
        this(referent, null);//referent:ThreadLocal的引用
    }
    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }
    
  • 在上面的代码中,我们可以看出,当前ThreadLocal的引用k被传递给WeakReference的构造函数,所以ThreadLocalMap中的key为ThreadLocal的弱引用。当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap就会存放一个记录,这个记录的key值为ThreadLocal的弱引用,value就是通过set设置的值。如果当前线程一直存在且没有调用该ThreadLocal的remove方法,如果这个时候别的地方还有对ThreadLocal的引用,那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,是不会释放的,就会造成内存泄漏。

  • 考虑这个ThreadLocal变量没有其他强依赖,如果当前线程还存在,由于线程的ThreadLocalMap里面的key是弱引用,所以当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用在gc的时候就被回收,但是对应的value还是存在的这就可能造成内存泄漏(因为这个时候ThreadLocalMap会存在key为null但是value不为null的entry项)。

  • 总结:THreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,在没有其他地方对ThreadLoca依赖,ThreadLocalMap中的ThreadLocal对象就会被回收掉,但是对应的不会被回收,这个时候Map中就可能存在key为null但是value不为null的项,这需要实际的时候使用完毕及时调用remove方法避免内存泄漏。

  • 5、ThreadLocalMap结构

    • ThreadLocalMap是ThreadLocal的一个静态内部类。里面的核心是一个Entry数组,Entry继承了WeakReference,在创建Entry的时候,将ThreadLocal对象设置成了弱引用。

    注意:ThreadLocalMap虽然是ThreadLocal里面的一个静态内部类,但是它的实例是放在Thread里面的

    
     static class ThreadLocalMap {
         /**
         * 初始容量,默认为16,必须为2的幂
         */
        private static final int INITIAL_CAPACITY = 16;
        /**
         * 表里entry的个数
         */
        private int size = 0;
        /**
         * Entry表,大小必须为2的幂
         */
        private Entry[] table;
        /**
         * Entry继承了WeakReference
         * Entry的构造方法中调用了super(k),将ThreadLocal对象设置成了弱引用
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
                /** value就是和ThreadLocal绑定的,为实际放入的值 */
                Object value;
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }
     }
    
    • 为什么要用弱引用?

    而弱引用是Java中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次GC。

    当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利。

    • 什么情况下会内存泄漏?

    我们把JVM的最大堆设置成100MB,运行下面的代码。
    用JProfiler查看内存使用情况,发现内存使用不断增大,直到抛出java.lang.OutOfMemoryError: Java heap space也就是OOM异常。

    202208230101.png

    • 分析如下:

    创建了一个核心线程数和最大线程数为5的线程池,这个保证了线程池里面随时都有5个线程在运行•模拟50个任务,每隔2秒往线程池里面加一个任务•任务:创建一个User对象user,给user的threadLocal赋值一个新创建的ThreadLocal对象,往这个ThreadLocal对象里面加一个5MB的Memory对象。

    2022082302.gif

    • ThreadLocal对象存在两个引用,实现代表强引用,虚线代表弱引用。•强引用因为引用对象被回收了引用不存在了•虚引用是不能阻止GC的回收的•最终Entry的key最终指向的是null,而value指向的还是占用5MB内存空间的Memory对象。•这个时候,存在一条ThreadRef->Thread->ThreadLocalMap->Entry->Memory的强引用链,导致Memory无法被回收,造成内存泄漏,最终导致OOM。

    • 通过上面的内存分配图,我们不能得出:

    • 如果线程运行完任务就结束了,ThreadRef->Thread->ThreadLocalMap->Entry->Memory这条引用链就不存在了,就不存在内存泄漏的问题了。

    • 但是现在的Java应用,为了节省开销,大部分都会采用线程池的模式。为了不造成内存泄漏,最简单有效的方法是使用后调用remove()方法将其移除。

    原文链接:https://blog.csdn.net/qq_25475445/article/details/128604233