【JDK源码】一文清晰明白的讲清楚多线程的ThreadLocal,避免因使用ThreadLocal导致内存泄漏

什么是ThreadLocal

在java的多线程并发执行过程中,为保证多个线程对变量的安全访问,可以将变量放到TrheadLocal类型的对象中,使变量在每个线程中都有独立值,不会出现一个线程读取变量时被另一个线程修改的现象。ThreadLocal类通常被翻译为“线程本地变量”类或者“线程局部变量”类。

ThreadLocal 位于JDK的java.lang核心包中,如果程序创建了一个ThreadLocal实例,那么在访问这个变量的值时,每个线程都会拥有一个独立、自己的本地值。ThreadLocal 代表的是线程本地变量,

ThreadLocal 类似专属于线程的变量,不受其他线程干扰,保存着线程的专属数据。当线程结束后,每个线程所拥有的那一个本地值也会被释放。在多线程并发操作ThreadLocal时,线程各自操作的是自己的本地值,从而规避了线程安全问题。

ThreadLocal 本地变量使用场景

线程隔离,作为无锁编程的重要方式

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

ThreadLocal常见使用场景有:
(1)数据库连接独享
为每个线程绑定一个数据库连接,使得这个数据库连接为线程所独享,从而避免数据库连接被混用而导致操作异常问题;以下代码是mybatis 通过ThreadLocal进行数据库连接的线程本地化存储,代码如下:

public class MySessionUtils {
    private static SqlSessionFactory sessionFactory;
    //static 静态代码,在类加载的时候执行一次,且只执行一次
    static{
		//  1 创建SqlSessionFactoryBuilder对象
        SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
		// 2 创建SqlSessionFactory对象
        InputStream inputStream = MySessionUtils.class.getClassLoader().getResourceAsStream("mybatis-config.xml");
        //加载核心配置文件
        sessionFactory = sqlSessionFactoryBuilder.build(inputStream);

    }

    //定义一个ThreadLocal集合,本质是Map<Thread,Object> map
    private    static final ThreadLocal<SqlSession> threadSession = new ThreadLocal<SqlSession>();

    public static SqlSession getSession() {

        //查找在local中,是否有对应的SqlSession
        SqlSession sqlSession = threadSession.get(); 

        if (sqlSession != null) {
            //有就直接返回给调用者使用
            return sqlSession;
        } else {

            //没有就创建一个新的,并且保存在local
            sqlSession = sessionFactory.openSession();
            //保存
            threadSession.set(sqlSession);

            return sqlSession;
        }
    }

    public static void commitAndClose() {
        //将来进行写操作,之后需要提交,我们定义的方法
        SqlSession session = threadSession.get();
        if (session != null) {
            session.commit();//提交
            session.close();//释放
            //避免内存泄漏
            threadSession.remove();
        }
    }

    public static void rollbackAndClose() {
        //将来进行写操作,之后需要提交,我们定义的方法
        SqlSession session = threadSession.get();
        if (session != null) {
            session.rollback();//回滚
            session.close();//释放
            //避免内存泄漏
            threadSession.remove();
        }
    }
    public static <T> T getMapper(Class clz) {
        return (T) getSession().getMapper(clz);
    }
}


(2)Session 数据管理
为每个线程绑定一个用户会话信息、数据库连接、HTTP请求等,这样一个线程所有调用到的处理函数都可以非常方便地访问这些资源。

跨函数传递数据

在同一线程的某地设置ThreadLocal存储数据,在随后的任何一个地方都可以获取到存储的数据,从而降低了类和方法之间通过参数或返回值获取数据的高内聚。

ThreadLocal在跨函数传递数据中经典的引用:

  • 用来传递请求过程中的用户ID;
  • 用来传递请求过程中的用户会话(Session);
  • 用来传递HTTP的用户请求实例HTTPRequest;
    -其他需要在函数之间频繁传递的数据;

通过ThreadLocal在函数之间传递用户信息、会话信息等封装的SessionHolder代码如下:

public class SessionHolder
{
    // session id  线程本地变量
    private static final ThreadLocal<String> sidLocal = new NamedThreadLocal<>("sidLocal");
    // session id store prefix 线程本地变量
    private static final ThreadLocal<String> sessionIDStore = new NamedThreadLocal<>("sessionIDStore");
    // session userIdentifier(这里为 user id) 线程本地变量
    private static final ThreadLocal<String> userIdentiferLocal = new NamedThreadLocal<>("userIdentiferLocal");
    // session 用户信息  线程本地变量
    private static final ThreadLocal<UserDTO> sessionUserLocal = new NamedThreadLocal<>("sessionUserLocal");
    // session  线程本地变量
    private static final ThreadLocal<HttpSession> sessionLocal = new NamedThreadLocal<>("sessionLocal");

    /**
     * 保存session
     *
     * @param session 待保存的session
     */
    public static void setSession(HttpSession session)
    {
        sessionLocal.set(session);
    }


    /**
     * 取得session
     *
     * @return 返回session
     */
    public static HttpSession getSession()
    {
        HttpSession session = sessionLocal.get();
        Assert.notNull(session, "session 未设置");
        return session;

    }
}

Thread的源码解析

ThreadLocal 源码提供的方法不多,主要是set(T value)、get()、remove()和nitialValue()四个方法;

在这里插入图片描述

set(T value)方法

set(T value) 方法用于设置ThreadLocal在当前线程的ThreadLocalM中对用的值,源码如下:

 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
            createMap(t, value);
        }
    }
    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
     void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

从源码得出set的执行流程如下:
在这里插入图片描述

get()方法

get()方法用于获取ThreadLocal 在当前线程的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) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 如果当前线程对应的map不存在, 或者map存在,但是当前threadlocal 实例没有
        //对应的"key-value对".返回初始值
        return setInitialValue();
    }
    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);
        }
        if (this instanceof TerminatingThreadLocal) {
            TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
        }
        return value;
    }

get()方法的执行流程如下:
在这里插入图片描述

remove()方法

remove方法用于在当前线程的ThreadLocalMap中移除ThreadLocal所对应的值,核心代码如下:

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

initialValue()方法

当“线程本地变量”在当前线程的ThreadLocalMap中尚未绑定值时,initialValue()方法用于获取初始值。如果没有调用set()直接调用get(),就会调用该方法,但是该方法只会被调用一次。默认情况下,initialValue()方法返回null。其源码如下:

 protected T initialValue() {
        return null;
    }

在JDK已经定义了ThreadLocal的内部SuppliedThreadLocal静态子类,并且提供了ThreadLocal.withInitial(…)静态工厂方法,方便大家在定义ThreadLocal实例时设置初始值回调函数。ThreadLocal.withInitial(…)静态工厂方法及其内部子类SuppliedThreadLocal的源码如下:

static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

        //保存钩子函数
        private final Supplier<? extends T> supplier;

        //传入钩子函数
        SuppliedThreadLocal(Supplier<? extends T> supplier) {
            this.supplier = Objects.requireNonNull(supplier);
        }

        @Override
        protected T initialValue() {
            return supplier.get();
        }
    }

ThreadLocalMap

ThreadLocal 的操作都是基于ThreadLocalMap进行的,ThreadLocalMap是ThreadLocal的一个静态内部类,实现的是一套简单的Map结构。
在这里插入图片描述

ThreadLocal源码中的get()、set()、remove() 方法都涉及ThreadLocalMap的方法调用。值得注意的是Entry的源码, Entry用于保存ThreadLocalMap的key-value对条目,Entry 使用了对ThreadLocal实例进行包装之后的弱引用作为key值。entry的源码如下:

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
               //使用WeakReference对key值进行包装
                super(k);
                value = v;
            }
        }

当GC发生时,无论内存够不够,仅有弱引用所指向的对象都会被回收。而拥有强引用指向的对象则不会被直接回收。所以ThreadLocalMap中Entry的Key使用弱引用的原因是在下次GC发生时,就可以使那些没有被其他强引用指向、仅被Entry的Key所指向的ThreadLocal实例能被顺利回收。并且,在Entry的Key引用被回收之后,其Entry的Key值变为null。后续当ThreadLocal的get()、set()或remove()被调用时,ThreadLocalMap的内部代码会清除这些Key为null的Entry,从而完成相应的内存释放。所以ThreadLocalMap中Entry的key为弱引用主要是为了解决内存泄漏。

ThreadLocal如何做到为每一个线程都保存一份独立的本地值

想要解决这个问题,我们就需要需要先来了解一下ThreadLocal 内部结构。

在jdk1.7版本以及1.7以下版本,ThreadLocal的内部结构是一个Map,key值为线程实例,value(本地值)为线程在“线程本地变量”中绑定的值。Map结构的拥有者为ThreadLocal,每一个ThreadLocal 实例拥有一个Map实例。如下图所示:
在这里插入图片描述

1.7版本的ThreadLocal内部结构存在的2个比较严重问题:
(1)浪费空间
“key-value对”数量与线程个数强关联,如果线程数量多,则ThreadLocalMap存储“key-value”entry 的数量也多,一般情况下,程序的ThreadLocal实例会比较少,而线程数较多。
(2)存货周期太长
由于ThreadLocalMap的拥有者是ThreadLocal,在Thread实例销毁后,ThreadLocal实例内部的ThreadLocalMap还是存在的。ThreadLocal本来是给Thread做线程隔离的,现在Thread不隔离了,但是ThreadLocal还在。

在jdk1.8版本后,ThreadLocal的内部结构发生了演进, 虽然还是使用Map结构,但是Map结构的拥有者为Thread实例,每一个Thead实例拥有一个Map实例, Map结构的Key值为ThreadLocal实例。如下图所示:
在这里插入图片描述

演进后的ThreadLocal内部结构的优势为:
(1)每个ThreadLocalMap存储的“key-value” 数量变少,现版本的ThreadLocalMap的key值为ThreadLocal实例,多线程情况下ThreadLocal实例比线程数少。
(2) 新版本的ThreadLocalMap 的拥有者为Thead,现在当Thread实例销毁后,ThreadLocalMap 也会随之销毁,在一定程度上减少内存消耗。

了解了ThreadMap的内部结构,那么我们就可以把ThreadLocal看作一个Map(jdk1.7版本之前),当工作线程Thread实例向本地变量保持某个值时,会以“key-value对”的形式保存在ThreadLocal内部的Map中,其中Key为线程Thread实例,Value为待保存的值。当工作线程Thread实例从ThreadLocal本地变量取值时,会以Thread实例为Key,获取其绑定的Value。

ThreadLocal 在什么情况下会发生内存泄漏

内存泄露是指不再用到的内存没有及时释放。而对于持续运行的服务进程必须及时释放内存,否则内存占用率越来越高,轻则影响系统性能,重则导致进程崩溃甚至系统奔溃。

ThreadLocal发生内存泄漏的前提条件有以下2点:
(1)线程长时间运行而没有被销毁,而线程池中的Thread实例特别容易满足这个条件;
(2)ThreadLocal 引用被置为null,且后续在同一Thread实例执行期间,没有发生对其他ThreadLocal实例的get()、set()、remove()操作。

那么在使用ThreadLocal时需要遵守以下2个原则避免内存泄漏问题的发生:
(1)尽量使用private static final 修饰THreadLocal实例。

(2)ThreadLocal使用完成后务必调用remove()方法。 最简单、有效避免ThreadLocal引发内存泄漏问题的方法。

为什么需要使用private static final修饰ThreadLocal变量

虽然ThreadLocal 变量是高性能无锁编程的一个重要的类,但是还是需要遵循使用规范。在阿里巴巴的java代码规范中要求如下:
【参考】 ThreadLocal 无法解决共享对象的更新问题,ThreadLocal对象建议使用static修饰,这个变量是针对一个线程线程内所有操作共享的,所以设置为静态变量,所以此类实例共享此静态变量,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类对象都可以操控这个变量。

使用static修饰ThreadLocal,节约内存空间

使用static的原因是因为线程内部的ThreadLocalMap的key就是ThreadLocal的实例,不同的线程可能使用同一个ThreadLocal实例的本地值,需要通过key来取,如果是static修饰,不同的keyi可同共享同一份ThreadLocal实例变量,节省空间,所以就使用static来修改, 使用过程如下图所示:
在这里插入图片描述

使用final修饰ThreadLocal,加强修饰

在此之前,我们先来了解一下final的作用,
(1) 修饰成员变量,说明该变量的值是不可变的。标注该变量只能进行一次赋值操作,且在运行过程中不可改变它的值;
(2)修饰方法入参,说明整个方法中,参数的值是不可变的。(PS:当入参为对象时,是可以改变引用对象中成员变量的值);
(3)修饰方法,说明该方法不能被覆盖,既不能被继承该类的子类重写;
(4)修饰类,说明该类是无法被继承的。

使用final 修饰ThreadLocal的原因如下:
(1) 尽量不要修改ThreadLocal变量的引用;因为使用场景之一就是在不同场景中获取数据,修改了ThreadLocal变量的引用,会导致不同场景之间的数据传递无法获取。

但是使用final static 修饰ThreadLocal有好处,但是同样也带了新问题:破坏了ThreadLocal的防止内存泄漏的机制。ThreadLocal 的防止内存泄漏的机制是通过弱引用防止内存泄漏的。

使用private修饰ThreadLocal,缩小使用范围

因为使用static final修饰ThreadLocal实例后,始终有强引用指向变量,破坏了弱引用规则,导致了ThreadLocalMap的Entry中key弱引用失效,破坏了ThreadLocalMap的内存泄漏机制。所以要避免ThreadLocal被外部类使用,可以提供封装的方法供外部使用。 所以使用private修饰ThreadLocal对象的主要目的是缩小使用范围,尽可能不让外部类引用,避免内存泄漏。

使用完ThreadLocal后调用remove()显性释放操作

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

总结:使用private与final修饰符主要是为了尽可能不让他人修改、变更ThreadLocal变量的引用,使用static修饰符主要是为了确保ThreadLocal实例的全局唯一。调用remove()方是简单、有效地避免ThreadLocal引发内存泄漏问题的方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

弯_弯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值