ThreadLocal(二) ThreadLocal内存泄漏原因,原理以及生产标准范例

写在正文前: 如果不理解ThreadLocal是什么? 应用场景的可以先看博主写的关于ThreadLocal的第一篇文章

传送门:ThreadLocal(一) ThreadLocal使用场景和API介绍

---------------------------------------------------------------------------------------------------------

好的, 我们开始!

为什么会造成内存泄漏

下面字字精华,请耐心读完 
我们上面说了ThreadLocal 本身并不存储值,它只是作为一个 key保存到ThreadLocalMap中,但是这里要注意的是它作为一个key用的是弱引用(什么是强引用,软引用,弱引用,虚引用)本处不做阐述。 
因为没有强引用链,弱引用在GC的时候可能会被回收。这样就会在ThreadLocalMap中存在一些key为null的键值对(Entry)。因为key变成null了,我们是没法访问这些Entry的,但是这些Entry本身是不会被清除的,为什么呢?因为存在一条强引用链。即线程本身->ThreadLocalMap->Entry也就是说,恰恰我们在使用线程池的时候,线程使用完了是会放回到线程池循环使用的。由于ThreadLocalMap的生命周期和线程一样长,如果没有手动删除对应key就会导致这块内存即不会回收也无法访问,也就是内存泄漏。 
其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。但是这些举动不能保证内存就一定会回收,因为可能这条线程被放回到线程池里后再也没有使用,或者使用的时候没有调用其get(),set(),remove()方法。

 

为什么使用弱引用,内存泄漏是否是弱引用的锅?

下面我们分两种情况讨论:

(1)key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

(2)key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set、get、remove的时候会被清除。

因此:内存泄漏归根结底是由于ThreadLocalMap的生命周期跟Thread一样长。如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

 

如何避免内存泄漏

1. 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。 

2. ThreadLocal变量用private static final 修饰, 保证强引用!
注意:并不是所有使用ThreadLocal的地方,都在最后remove(),他们的生命周期可能是需要和项目的生存周期一样长的,所以要进行恰当的选择,以免出现业务逻辑错误!
 

附:强引用-软引用-弱引用

  • 强引用:普通的引用,强引用指向的对象不会被回收;
  • 软引用:仅有软引用指向的对象,只有发生gc且内存不足,才会被回收;
  • 弱引用:仅有弱引用指向的对象,只要发生gc就会被回收

-------------------------------------------------  下面是实例环节, 所有代码都经过博主测试,测试代码已经上github------------------------

1. 创建ThreadLocal对象, 这边封装的是业务上下文

package com.zz.amqp1.bean.common;

/**
 * Description:业务会话
 * User: zhouzhou
 * Date: 2018-12-11
 * Time: 1:30 PM
 */
public class BizSession {

    private static ThreadLocal<BizContext> INSTANCE = new ThreadLocal<BizContext>(){
        @Override
        protected BizContext initialValue() {
            // 这边初始化当前会话的环境并且初始化
            BizContext context = new BizContext();
            context.setSystemEnv("dev");
            return context;
        }
    };



    /**
     * 获取当前会话
     * @return 会话实例
     */
    public static BizContext currentSession(){
        return INSTANCE.get();
    }

    /**
     * 创建会话
     * @param session 会话实例
     */
    public static void store(BizContext session){
        INSTANCE.set(session);
    }

    /**
     * 销毁会话
     */
    public static void destroy (){
        INSTANCE.remove();
    }

}

2. 附上BizContext的POJO类

/**
 * Description:业务上下文
 * User: zhouzhou
 * Date: 2018-12-11
 * Time: 2:07 PM
 */
@Data
public class BizContext {

    private Student student;

    private String systemEnv;
}

3. 一般我们不直接操作业务会话类, 通过工具操作

/**
 * Description: 业务会话工具类
 * User: zhouzhou
 * Date: 2018-12-11
 * Time: 2:03 PM
 */
public class BizSessionUtils {

    public static void setStudent(Student student){
        BizSession.currentSession().setStudent(student);
    }

    public static BizContext getBizContext(){
        return BizSession.currentSession();
    }

}

4. 最后附上测试类:

package com.zz.amqp1.multithread;

import com.zz.amqp1.bean.Student;
import com.zz.amqp1.bean.common.BizSession;
import com.zz.amqp1.utils.BizSessionUtils;

import java.util.Random;

/**
 * Description:ThreadLocal测试类, 线程共享变量
 * <p>
 * User: zhouzhou
 * Date: 2018-12-11
 * Time: 10:18 AM
 */
public class ThreadLocalTest {

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            // 创建两个线程, 测试每个线程的ThreadLocal是否独立, 数据是否安全
            new Thread(() -> {
                Student student = Student.builder()
                        .sex("男")
                        .age(new Random().nextInt(20) + 18)
                        .name("学生" + new Random().nextInt(5)).build();
                BizSessionUtils.setStudent(student);
                System.out.println(String.format("{%s}中存入对象为{%s}", getThreadName(), BizSession.currentSession()));
                System.out.println(getThreadName() + String.format("开始获取学生:{%s},获取环境{%s}", OrderHelper.getStudent(), OrderHelper.getEnv()));

                // 销毁session
                System.out.println(getThreadName() + "正在销毁当前会话");
                BizSession.destroy();
                System.out.println(getThreadName() + "销毁完毕, 尝试获取线程对象:" + BizSessionUtils.getBizContext());
                }, "线程" + (i + 1)).start();
        }
    }


    public static String getThreadName() {
        return Thread.currentThread().getName();
    }

    /**
     * order helper support two functions:
     * 1. get student in current session
     * 2. get current system environment
     */
    static class OrderHelper {

        /**
         * get current Student in current Session
         *
         * @return Student
         */
        public static Student getStudent() {
            return BizSession.currentSession().getStudent();
        }

        /**
         * return current System Environment
         *
         * @return system environment
         */
        public static String getEnv() {
            return BizSession.currentSession().getSystemEnv();
        }
    }

}

main方法运行结果如下:


{线程2}中存入对象为{BizContext(student=Student(name=学生1, age=30, sex=男), systemEnv=dev)}
{线程1}中存入对象为{BizContext(student=Student(name=学生2, age=26, sex=男), systemEnv=dev)}
线程1开始获取学生:{Student(name=学生2, age=26, sex=男)},获取环境{dev}
线程1正在销毁当前会话
线程1销毁完毕, 尝试获取线程对象:BizContext(student=null, systemEnv=dev)
线程2开始获取学生:{Student(name=学生1, age=30, sex=男)},获取环境{dev}
线程2正在销毁当前会话
线程2销毁完毕, 尝试获取线程对象:BizContext(student=null, systemEnv=dev)

为什么销毁了会话, 还能获得呢? 因为咱们重写了ThreadLocal的init方法,所以, 当使用get方法时候为空,则默认返回调用init方法创建出来的对象!

另外:附上博主代码的github地址: https://github.com/zjhzzhouzhou/StudyProject  ,直接搜索类名即可, 喜欢的给个star谢谢

 

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值