写在正文前: 如果不理解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谢谢