并发编程——ThreadLocal篇

        在学习项目的时候突然用到了阿里巴巴开源的TransmittableThreadLocal,想着在学习这个TTL之前先把ThreadLocal给深入探究一下,以便能够清晰的对比两者之间的异同点。本文是基于《Java高并发核心编程+卷2》一书来去做简要读后观感的。

目录

一.ThreadLocal的简单介绍

二.ThreadLocal的基本使用

三.ThreadLocal的使用场景

       (1)线程隔离

       (2)跨函数传递数据

四.使用ThreadLocal进行线程隔离

五.使用ThreadLocal进行跨函数数据传递

六.ThreadLocal内部结构的迭代

七.ThreadLocal源码分析

        1.set(T value)方法

        2.get()方法

        3.remove()方法

        4.initialValue()方法

八.ThreadLocalMap源码分析

        1.ThreadLocalMap的主要成员变量

        2.Entry的Key需要使用弱引用

        3.编程规范推荐使用static final修饰ThreadLocal对象


一.ThreadLocal的简单介绍

        ThreadLocal类翻译过来叫做“线程本地变量”类或者“线程局部变量”类。顾名思义,在多个线程对ThreadLocal类型的对象中的变量进行访问时,其每个线程只能去访问自己的那个独立变量,而不会出现一个线程读取到另一个线程修改变量后的值,这使得在多线程并发执行的场景中做到了线程安全

二.ThreadLocal的基本使用

        ThreadLocal它位于JDK的java.lang核心包中,如果程序创建了一个ThreadLocal实例,那么在访问这个实例的值时,每个线程都会拥有一个独立的,自己的本地值,不受其他线程干扰,保存着线程的专属数据。当线程结束后,每个线程所拥有的那个本地值就会被释放。在多线程并发操作“线 程本地变量”的时候,线程各自操作的是自己的本地值,从而规避了 线程安全问题。

        那么ThreadLocal是如何做到为每个线程存有一份独立的本地值呢?一个ThreadLocal实例可以形象地理解为一个Map(早期版本的 ThreadLocal是这样设计的)。其中ThreadLocal有3种常用的成员方法来进行对本地值的操作,

  1. set(T value):当当前工作线程Thread实例向“线程本地变量”也就是这个ThreadLocal实例保存某个值value时,会以“Key-Value对”(即键-值对)的形式保存在 ThreadLocal实例内部的Map中,其中Key为当前线程Thread实例,Value为待保存的值。
  2. T get():当当前工作线程Thread实例从他的ThreadLocal本地变量中获取某个值value时,会以当前工作线程Thread实例为Key,获取其绑定的Value
  3. remove():当当前工作线程Thread实例从他的ThreadLocal本地变量中移除绑定值value时,会 以当前工作线程Thread实例为Key,移除其绑定的Value。

同样一个ThreadLocal实例(早期版本)内部结构的形象展示大致如下图所示。

接下来举一个例子,通过ThreadLocal的3个成员方法增加,获取,移除来操作“线程本地变量”,具体代码如下:

@Data
public class Demo {
    //用static修饰表示类变量,能够跨多个实例共享和更新计数,
    //此处用来跟踪Demo类的实例数量
    private static final AtomicInteger num=new AtomicInteger(0);
    //对象的编号
    int index=0;
    //对象的内容
    int context=10;
    public Demo(){
        //用来递增编号
        index=num.incrementAndGet();
    }
    public String toString(){
        return index+"@Demo{"+context+'}';
    }
}
public class ThreadLocalTest {
    //定义线程本地变量
    private static final ThreadLocal<Demo> LOCAL_DEMO = new ThreadLocal<Demo>();

    public static void main(String[] args) {
        //创建线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                5,
                10,
                1L,
                TimeUnit.MINUTES,
                new LinkedBlockingQueue<>(),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );
        //提交5个任务,将会创建5个线程
        for (int i = 0; i < 5; i++) {
            executor.execute(()->{
                //获取“线程本地变量”中当前线程所绑定的值
                if (LOCAL_DEMO.get() == null) {
                    //设置“线程本地变量”中当前线程所绑定的值
                    LOCAL_DEMO.set(new Demo());
                }
                System.out.println("初始的本地值:" +LOCAL_DEMO.get().toString());

                //每个线程执行10次
                for (int j = 0; j < 10; j++) {
                    Demo demo = LOCAL_DEMO.get();
                    demo.setContext(demo.getContext() + 1);
                    //保证5个线程的初始本地值都打印完成
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                System.out.println("累加10次之后的本地值:" +LOCAL_DEMO.get().toString());

                //删除“线程本地变量”中当前线程所绑定的值
                LOCAL_DEMO.remove();
                //这点对于线程池中的线程尤其重要
            });
        }
    }
}

运行以上示例,其结果如下:

初始的本地值:5@Demo{Context=10}
初始的本地值:3@Demo{Context=10}
初始的本地值:2@Demo{Context=10}
初始的本地值:1@Demo{Context=10}
初始的本地值:4@Demo{Context=10}
累加10次之后的本地值:3@Demo{Context=20}
累加10次之后的本地值:2@Demo{Context=20}
累加10次之后的本地值:1@Demo{Context=20}
累加10次之后的本地值:4@Demo{Context=20}
累加10次之后的本地值:5@Demo{Context=20}

        通过输出的结果可以看出,在“线程本地变量”(LOCAL_DEMO) 中,每一个线程都绑定了一个独立的值(Demo对象),这些值对象是 线程的私有财产,可以理解为线程的本地值,线程的每一次操作都 是在自己的同一个本地值上进行的,从例子中线程本地值的index始 终一致可以看出,每个线程操作的是自己的Demo对象

        如果线程尚未在本地变量(如LOCAL_DEMO)中绑定一个值,直接通过调用get()方法获取本地值会获取到一个空值,此时可以通过 调用set()方法设置一个值作为初始值,具体的代码如下

//获取“线程本地变量”中当前线程所绑定的值
if (LOCAL_DEMO.get() == null) {
    //设置“线程本地变量”中当前线程所绑定的值
    LOCAL_DEMO.set(new Demo());
}

        在当前线程尚未绑定值时,如果希望从线程本地变量获取到初始值,而且不想采用以上的“判空后设值”这种相对烦琐的方式,可以 调用ThreadLocal.withInitial(…)静态工厂方法,在定义ThreadLocal对象 时设置一个获取初始值的回调函数,然后在线程尚未绑定值而直接从“线程本地变量”获取值时,将会取得回调函数被调用之后所返回的值。具体的代码如下:

 private static final ThreadLocal<Demo> LOCAL_DEMO=ThreadLocal.
                             withInitial(()->new Demo());

三.ThreadLocal的使用场景

        ThreadLocal是解决线程安全的一个较好的方案,它通过为每个线程提供一个独立的本地值去解决并发访问带来的冲突问题。在很多时候,使用ThreadLocal比直接使用同步机制(如synchronized)解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

        ThreadLocal的使用场景大值分为两类

        (1)线程隔离

        ThreadLocal的主要价值在于线程隔离。首先就是它的定义嘛,ThreadLocal中的数据只属于当前线程,其本地值对别的线程是不可见的,在多线程环境下,可以防止自己的变量被其他线程篡改。其次在功能上,由于各个线程之间的数据相互隔离,避免了同步加锁带来的性能损失,大大提升了并发性的性能。

        ThreadLocal在线程隔离的常用案例为:可以为每个线程绑定一个用户会话信息、数据库连接、HTTP请求等,这样一个线程所有调用到的处理函数都可以非常方便地访问这些资源,而不用去调用多层方法繁琐的去获取。

        常见的ThreadLocal使用场景为数据库连接独享、Session数据管理等。

        在“线程隔离”场景中,使用ThreadLocal的典型案例为:可以为每个线程绑定一个数据库连接,使得这个数据库连接为线程所独享,从而避免数据库连接被混用而导致操作异常问题,在下文就有一个在Hibernate中的实际应用。

       (2)跨函数传递数据

        通常用于同一个线程内,跨类、跨方法传递数据时,如果不用ThreadLocal,那么相互之间的数据传递势必要靠返回值和参数,这样无形之中增加了这些类或者方法之间的耦合度

        由于ThreadLocal的特性,同一线程在某些地方进行设置,在随后的任意地方都可以获取到。线程执行过程中所执行到的函数都能读写ThreadLocal变量的线程本地值,从而可以方便地实现跨函数的数据传递。使用ThreadLocal保存函数之间需要传递的数据,在需要的地方直接获取,也能避免通过参数传递数据带来的高耦合。

        在“跨函数传递数据”场景中使用ThreadLocal的典型案例为:可以为每个线程绑定一个Session(用户会话)信息,这样一个线程所有调用到的代码都可以非常方便地访问这个本地会话,而不需要通过参数传递。

四.使用ThreadLocal进行线程隔离

        ThreadLocal在“线程隔离”典型运用场景为“数据库连接独享”。例如ORM框架其中之一的Hibernate中就是通过ThreadLocal来进行数据库连接(Session)的“线程本地化”存储,主要代码如下:

    private static final ThreadLocal threadSession = new
            ThreadLocal();

    public static Session getSession() throws
            InfrastructureException {
        Session s = (Session) threadSession.get();
        try {
            if (s == null) {
                s = getSessionFactory().openSession();
                threadSession.set(s);
            }
        } catch (HibernateException ex) {
            throw new InfrastructureException(ex);
        }
        return s;
    }

        Hibernate对数据库进行了封装,一个Session就代表了一个数据库连接。通过上面的代码可看出,首先定义了一个ThreadLocal类型的静态变量,然后在Hibernate的getSession()方法中,首先尝试从“线程本地变量”中尝试获取Session,然后判断是否存在,如果不存在那就调用sessionFactory().openSession()来创建一个Session,然后再将其存入“线程本地变量”中。显然这个Session相当于线程私有的变量,而不是所有线程共用的,其他线程中是取不到这个Session的。

        一般来说,完成一个数据库操作时候就会程序就会将Session给关闭,从而节省数据库连接资源。而如果Session是共享的,当某个线程使用完成之后就会直接将Session关闭,其他线程在操作Session时就会报错。所以 Hibernate通过ThreadLocal非常简单地实现了数据库连接的安全使用。

五.使用ThreadLocal进行跨函数数据传递

        ThreadLocal在“跨函数数据传递”场景的典型应用有很多:

  1. 用来传递请求过程中的用户ID。
  2. 用来传递请求过程中的用户会话(Session)。
  3. 用来传递HTTP的用户请求实例HttpRequest。
  4. 其他需要在函数之间频繁传递的数据。

六.ThreadLocal内部结构的迭代

        在早期的JDK版本中,ThreadLocal的内部结构是一个Map,其中每一个线程实例作为Key线程在“线程本地变量”中绑定的值为Value(本地值)。早期版本中的Map结构,其拥有者为ThreadLocal每一个ThreadLocal实例都拥有一个Map实例

        而在JDK 8版本中,与之前相比发生了较大变化。ThreadLocal的内部结构虽然还是使用了Map结构,但是Map结构的拥有者已经发生了变化其拥有者为Thread(线程)实例每一个Thread实例拥有一个Map实例(也就是ThreadLocalMap)。如果给一个Thread创建多个ThreadLocal实例,那么当前ThreadLocalMap中就会有多个“Key-Value”键值对,其中ThreadLocal实例为Key,本地数据为Value。每一个线程Thread获取本地值时,都会将ThreadLocal实例作为Key从自己拥有的ThreadLocalMap中获取值,别的线程无法访问到自己的Map实例,自己也无法访问别人的,以此达到相互隔离,互不干扰。

        从代码层面来开,新版本的ThreadLocalMap还是在ThreadLocal类里边,并由ThreadLocal负责ThreadLocalMap的获取,创建,并从中获取,设置本地值。所以ThreadLocalMap还寄存在ThreadLocal内部,并没有迁移到Thread内部。

        如下图,左边为JDK8之前的一个ThreadLocalMap实例内部结构右边为JDK8之后的

总结一下:

        与早期版本的ThreadLocalMap实现相比,新版本的主要变化为:

  1. 拥有者发生了变化:新版本的ThreadLocalMap拥有者为Thread早期版本的ThreadLocalMap拥有者为ThreadLocal。
  2. Key发生了变化:新版本的Key为ThreadLocal实例,早期版本的Key为Thread实例。

        与早期版本的ThreadLocalMap实现相比,新版本的主要优势 为:

  1. 每个ThreadLocalMap存储的“Key-Value对”数量变少。早期版本的“Key-Value对”数量与线程个数强关联,若线程数量多,则 ThreadLocalMap存储的“Key-Value对”数量也多。新版本的 ThreadLocalMap的Key为ThreadLocal实例,多线程情况下ThreadLocal 实例比线程数少。
  2. 早期版本ThreadLocalMap的拥有者为ThreadLocal,在 Thread(线程)实例销毁后,ThreadLocalMap还是存在的;新版本的ThreadLocalMap的拥有者为Thread,现在当Thread实例销毁后, ThreadLocalMap也会随之销毁,在一定程度上能减少内存的消耗。

七.ThreadLocal源码分析

        ThreadLocal常用的无非四个方法其分别是:set(T value)方法、 get()方法、remove()方法和initialValue()方法。

        1.set(T value)方法

        set(T value)方法用于设置“线程本地变量”在当前线程的 ThreadLocalMap中对应的值,相当于设置线程本地值,其核心源码 如下:

        public void set (T value){
            //获取当前线程对象
            Thread t = Thread.currentThread();

            //获取当前线程的ThreadLocalMap 成员
            ThreadLocalMap map = getMap(t);

            //判断map是否存在
            if (map != null) {
                //value被绑定到threadLocal实例
                map.set(this, value);
            } else {
                // 如果当前线程没有ThreadLocalMap成员实例
                // 创建一个ThreadLocalMap实例,然后作为成员关联到
                //t(thread实例)
                createMap(t, value);
            }
        }

        // 获取线程t的ThreadLocalMap成员
        ThreadLocalMap getMap (Thread t){
            return t.threadLocals;
        }

        // 线程t创建一个ThreadLocalMap成员
        // 并为新的Map成员设置第一个“Key-Value对”,Key为当前的
        //ThreadLocal实例

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

        通过以上源码可以看出set(T value)方法的执行流程,大致如下:

  1. 首先通过Thread.currentThread()方法来获取当前线程对象。
  2. 然后调用getMap()方法来得到该线程下的ThreadLocalMap对象。
  3. 然后判断其Map对象是否不为空,如果存在那就就将Value设置到map中,当前的 ThreadLocal作为Key。
  4. 如果Map为空,就会调用createMap方法为该线程创建一个ThreadLocalMap对象,并为此设置一个“Key-Value对”,Key为当前的ThreadLocal实例,Value为set()方法的参数value值。

        2.get()方法

        get()方法用于获取“线程本地变量”在当前线程的ThreadLocalMap中对应的值,相当于获取线程本地值,其核心源码 如下:

        public T get () {
            // 获得当前线程对象
            Thread t = Thread.currentThread();
            // 获得线程对象的ThreadLocalMap 内部成员
            ThreadLocalMap map = getMap(t);

            // 如果当前线程的内部map成员存在
            if (map != null) {
                // 以当前ThreadLocal为Key,尝试获得条目
                ThreadLocalMap.Entry e = map.getEntry(this);
                // 条目存在
                if (e != null) {
                    T result = (T) e.value;
                    return result;
                }
            }
            // 如果当前线程对应的map不存在
            // 或者map存在,但是当前ThreadLocal实例没有对应的“KeyValue对”,返回初始值
            return setInitialValue();
        }

        // 设置ThreadLocal关联的初始值并返回
        private T setInitialValue () {
            // 调用初始化钩子函数,获取初始值
            T value = initialValue();
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
            return value;
        }

        通过以上源码可以看出T get()方法的执行流程,大致如下:

  1. 先尝试获取当前线程,然后根据当前线程获取其ThreadLocalMap成员,并暂存于map变量。
  2. 如果map不为空,就以当前ThreadLocal为key从map中获取Entry条目。
  3. 如果Entry条目不为空,再得到其value,最终返回其Value。
  4. 如果Entry为空,就通过调用initialValue初始化钩子函数获取ThreadLocal初始值,并设置在map中。如果map不存在,还会给 当前线程创建新ThreadLocalMap成员,并绑定第一个“Key-Value 对”。

        3.remove()方法

        remove()方法用于在当前线程的ThreadLocalMap中移除“线程本地变量”所对应的值,其核心源码如下:

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

        4.initialValue()方法

        当“线程本地变量”在当前线程的ThreadLocalMap中尚未绑定值时,initialValue()方法用于获取初始值。其源码如下:

        protected T initialValue() {
            return null;
        }

        如果没有调用set()方法而直接调用get()方法,就会调用该initialValue方法去返回null,如果你不想返回null,可以继承ThreadLocal去覆盖此方法。

        但是没必要去继承ThreadLocal去覆盖此方法来获取默认返回值。JDK内部已经定义了一个SuppliedThreadLocal静态子类,并提供了ThreadLocal.withInitial()静态工厂方法,方便大家在定义ThreadLocal实例的时候就可以设置初始值回调函数。使用工厂方法构造ThreadLocal实例的代码如下:

    //定义线程本地变量
    private static final ThreadLocal<Demo> LOCAL_FOO =ThreadLocal.withInitial(()
            -> new Demo());

八.ThreadLocalMap源码分析

        ThreadLocal的操作都是基于ThreadLocalMap展开的,而 ThreadLocalMap是ThreadLocal的一个静态内部类,其实现了一套简单的Map结构(比HashMap简单)。

        1.ThreadLocalMap的主要成员变量

        ThreadLocalMap的成员变量与HashMap的成员变量非常类似, 其内部的主要成员如下:

        public class ThreadLocal<T> {
            // 省略其他
            static class ThreadLocalMap {
                // Map的条目数组,作为哈希表使用
                private Entry[] table;
                // Map的条目初始容量16
                private static final int INITIAL_CAPACITY = 16;
                // Map的条目数量
                private int size = 0;
                // 扩容因子
                private int threshold;

                // Map的条目类型,一个静态的内部类
                // Entry 继承子WeakReference, Key为ThreadLocal实例
                static class Entry extends
                        WeakReference<ThreadLocal<?>> {
                    Object value; //条目的值

                    Entry(ThreadLocal<?> k, Object v) {
                        super(k);
                        value = v;
                    }
                }
                // 省略其他
            }

        ThreadLocal源码中的get()、set()、remove()方法都涉及ThreadLocalMap的方法调用,主要调用了ThreadLocalMap的如下几 个方法:

  • set(ThreadLocal key,Object value):向Map实例设置 “Key-Value对”。
  • getEntry(ThreadLocal):从Map实例获取Key(ThreadLocal 实例)所属的Entry。
  • remove(ThreadLocal):根据Key(ThreadLocal实例)从Map实例移除所属的Entry。

        在这里举例一个set()方法中的源码做出简单分析:

        private void set (ThreadLocal < ? > key, Object value){
            Entry[] tab = table;
            int len = tab.length;

            //根据key的HashCode,找到key在数组上的槽点i
            int i = key.threadLocalHashCode & (len - 1);

            // 从槽点i开始向后循环搜索,
            // 找空余槽点(空余位置)或者找现有槽点
            // 若没有现有槽点,则必定有空余槽点
            // ,因为没有空间时会扩容

            for (Entry e = tab[i]; e != null;
                 e = tab[i =
                         nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                //找到现有槽点:Key值为ThreadLocal实例
                if (k == key) {
                    e.value = value;
                    return;
                }
                //找到异常槽点:槽点被GC掉,重设Key值和Value值
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //没有找到现有的槽点,增加新的Entry
            tab[i] = new Entry(key, value);
            //设置ThreadLocal数量
            int sz = ++size;

            //清理Key为null的无效Entry
            //没有可清理的Entry,并且现有条目数量大于扩容因子
            //值,进行扩容
            if (!cleanSomeSlots(i, sz) && sz >=threshold){
                rehash();
            }
        }

        2.Entry的Key需要使用弱引用

        Entry用于保存ThreadLocalMap的Key-Value对条目,但是Entry使用了对ThreadLocal实例进行包装之后的弱引用(WeakReference)作 为Key,其代码如下:

        // Entry 继承了WeakReference,并使用WeakReference对Key进行包装
        static class Entry extends WeakReference<ThreadLocal<?>>
        {
            Object value; //值
            Entry(ThreadLocal<?> k, Object v) {
                super(k); //使用WeakReference对Key进行包装
                value = v;
            }
        }

        那为什么Entry需要使用弱引用对key进行包装,而不是直接使用ThreadLocal实例作为key呢?举一个例子,这里有一个方法funcA()创建了一个“线程本地变量”,具体如下:

        public void funcA()
        {
            //创建一个线程本地变量
            ThreadLocal local = new ThreadLocal<Integer>();
            //设置值
            local.set(100);
            //获取值
            local.get();
            //函数末尾
        }

        当线程a执行funcA()方法到末尾时,其线程相关的JVM栈内存以及内部ThreadLocalMap成员的结构如下图 :

        线程a调用funcA()方法新建了一个ThreadLocal实例,使用local局部变量指向这个实例,并且此local是强引用;在调用local.set(100)之后,线程a的ThreadLocalMap成员内部会新建一个Entry实例,其key以弱引用包装的方式指向ThreadLocal实例

        当线程a执行完func()方法之后,func()方法栈帧将被销毁,强引用的local的值也就没有了,但此时线程的ThreadLocalMap中对应的Entry的Key引用还指向ThreadLocal实例,如果Entry的Key引用是强引用,就会导致key引用指向的ThreadLocal实例以及Value值都不能被GC回收,这将导致严重的内存泄漏问题。

        什么是弱引用呢?仅有弱引用(Weak Reference)指向的对象只能生存到下一次垃圾回收之前。换句话说,当GC发生时,无论内存够不够,仅有弱引用所指向的对象都会被回收。而拥有强引用指向的对象则不会被直接回收。

        由于ThreadLocalMap中Entry的Key使用了弱引用,在下次GC发生时,就可以使那些没有被其他强引用指向、仅被Entry的Key所指向 的ThreadLocal实例能被顺利回收。并且,在Entry的Key引用被回收之 后,其Entry的Key值变为null。后续当ThreadLocal的get()、set()或 remove()被调用时,ThreadLocalMap的内部代码会清除这些Key为null的Entry,从而完成相应的内存释放。

        总结一下,使用ThreadLocal会发生内存泄漏的前提条件如下:

  • 线程长时间运行而没有被销毁。线程池中的Thread实例很容易满足此条件。
  • ThreadLocal引用被设置为null,且后续在同一Thread实例执行期间,没有发生对其他ThreadLocal实例的get()、set()或remove() 操作。只要存在一个针对任何ThreadLocal实例的get()、set()或 remove()操作,就会触发Thread实例拥有的ThreadLocalMap的Key为 null的Entry清理工作,释放掉ThreadLocal弱引用为null的Entry。

        综合以上两点可以看出:使用ThreadLocal出现内存泄漏还是比较容易的。但是一般公司对如何使用ThreadLocal都有编程规范,只要大家按照规范编写程序,也没有那么容易发生内存泄漏。

        3.编程规范推荐使用static final修饰ThreadLocal对象

        ThreadLocal实例作为ThreadLocalMap的Key,针对一个线程内的所有操作是共享的,所以建议设置static修饰符,以便被所有的对象共享。由于静态变量会在类第一次被使用时装载, 只会分配一次存储空间,此类的所有实例都会共享这个存储空间, 所以使用static修饰ThreadLocal就会节约内存空间。另外,为了确保 ThreadLocal实例的唯一性,除了使用static修饰之外,还会使用final 进行加强修饰,以防止其在使用过程中发生动态变更

        然而使用static、final修饰ThreadLocal实例也会带来副作用,使得Thread实例内部的ThreadLocalMap中Entry的Key在Thread实例的生命期内将始终保持为非null,从而导致Key所在的 Entry不会被自动清空,这就会让Entry中的Value指向的对象一直存在强引用,于是Value指向的对象在线程生命期内不会被释放,最终导致内存泄漏。所以,在使用完static、final修饰的ThreadLocal实例之后,必须调用remove()来进行显式的释放操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值