讲述一个以前遇到的问题,问题的现象是这样的,通过CRM操作我们接口时因为没有登录,是不会有用户上下文信息的,但是通过日志发现也打印了上下文信息,造成这种情况可能是我们自己用户登录自己的app然后上下文中保存了在了threadlocal中,然后没有释放,因为tomcat线程池的原因,导致线程复用,crm操作时在复用了这个线程就导致打印出来了上下文信息。
查找了一番引用的基础依赖,发现了在内部starter基础依赖中有个filter,filter里会从header头中解析获取用户上线文信息,然后设置到ContextHolder里,ContextHolder里可以看作是保存了一个ThreadLocal变量(里面采用策略模式,将保存Context动作抽象出来了,默认是以ThreadLocal存储Context的策略),这样在每个线程中就可以通过ContextHolder获取到用户上线文。但是这个filter里竟然没有用完的时候清空ContextHolder里的threadlocal(在filterChain.doFilter后没有删除操作)。
有问题(简化后)的结构
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 自己简化的操作 仅供展示
String user = AttributeHelp.getHeader(Xheader.X_MAN, request, null);
if (user != null) {
user = new String(user.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
JSONObject cu = JSONObject.parseObject(user);
ContextHolder.getContext().setAuthentication(new ContextUser().setUser(cu).setId(cu.getLong("id")).setOrgId(cu.getLong("orgId"))
.setUsername(cu.getString("username")).setRealName(cu.getString("realName")).setAccountType(cu.getInteger("accountType")));
}
filterChain.doFilter(request, response);
}
修改后如下
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 自己简化的操作 仅供展示
String user = AttributeHelp.getHeader(Xheader.X_MAN, request, null);
if (user != null) {
user = new String(user.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
JSONObject cu = JSONObject.parseObject(user);
ContextHolder.getContext().setAuthentication(new ContextUser().setUser(cu).setId(cu.getLong("id")).setOrgId(cu.getLong("orgId"))
.setUsername(cu.getString("username")).setRealName(cu.getString("realName")).setAccountType(cu.getInteger("accountType")));
}
filterChain.doFilter(request, response);
// 用完清除线程的threadlocal
ContextHolder.clearContext();
}
在filterChain.doFilter后添加一个清空threadlocal的操作就完事。。用完就删除
都在讲threadlocal,用完就清空,不清空就会造成内存泄漏,这个虽然也造成了内存泄漏,但是因为数量很少,tomcat线程数默认也就10~200个不会造成很大内存占用,而且如果都是自己的app登录的话都是有上下文的,线程内的上下文信息也会一直的变更也无所谓,但是也要养成好的习惯,用完就删除,万一造成了内存泄漏导致系统崩溃就gg喽~
介绍一下ThreadLocal原理,增加理解。
每个Thread对象中都保存着一个ThreadLocal.ThreadLocalMap类型的变量threadLocals,调用ThreadLocal实例的set方法时会将ThreadLocal这个实例,和set入参当作键值对存储到当前调用线程的threadLocals中。
类似于map,存储Threadlocal实例作为弱引用key,object对象作为value。key是弱引用,弱引用是一旦发生垃圾收集行为,不管内存够不够,都会进行收集,也就是说GC后,key如果没有其他变量的强引用,key就消失了,但是value是强引用,一直在内存中。
几个重要的方法
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取线程Thread的threadLocals变量
ThreadLocalMap map = getMap(t);
if (map != null) {
// 获取Map中的entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果没有设置初始值null,返回
return setInitialValue();
}
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取线程Thread的treadLocals变量
ThreadLocalMap map = getMap(t);
if (map != null)
// 设置值,this代表threadLocal,里面会用this,和value构造key,value的Entry实体
map.set(this, value);
else
createMap(t, value);
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
对照实验1:确定剩余没有被清空的数量,因为是强引用GC的话也不会清理掉arr数据的内容,看到第一次gc有15195k保留
public static void main(String[] args) {
// vm参数 -verbose:gc,可以控制台看到gc内容
int[] arr = new int[1024*60*60];
System.gc();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
对照实验2:增加weakReference引用没有对结果有太大的影响,因为数组实例还是被arr对象引用强引用连接着。
public static void main(String[] args) {
// vm参数 -verbose:gc,可以控制台看到gc内容
int[] arr = new int[1024*60*60];
WeakReference weakReference = new WeakReference(arr);
System.gc();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
对照实验3:因为在作用范围内将arr对象引用断开,数组实例是被weakReference弱引用连着,gc的时候数组实例对象被回收。
public static void main(String[] args) {
// vm参数 -verbose:gc,可以控制台看到gc内容
int[] arr = new int[1024*60*60];
WeakReference weakReference = new WeakReference(arr);
arr = null;
System.gc();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
MyEntry模拟ThreadLocalMap中的Entry,MyEntry采用int数组代替threadlocal,被gc清除的时候可以清楚看到内存变化。
static class MyEntry extends WeakReference {
private int[] arrValue;
public MyEntry(int[] key, int[] arrValue) {
super(key);
this.arrValue = arrValue;
}
}
对照实验一:
public static void main(String[] args) {
// vm参数 -verbose:gc,可以控制台看到gc内容
// arr 代替Threadlocal,被gc清除时可以清楚看到内存变化
int[] arr = new int[1024*60*60];
int[] arrValue = new int[1024*60*60];
MyEntry myEntry = new MyEntry(arr, arrValue);
System.gc();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
因为arr和arrValue都保留着强引用,所以gc的时候不会清除掉
对照实验二:
将arr和arrValue设置为null,断开强引用。
public static void main(String[] args) {
// vm参数 -verbose:gc,可以控制台看到gc内容
// arr 代替Threadlocal,被gc清除时可以清楚看到内存变化
int[] arr = new int[1024*60*60];
int[] arrValue = new int[1024*60*60];
MyEntry myEntry = new MyEntry(arr, arrValue);
arr = null;
arrValue = null;
System.gc();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
结果显示MyEntry的key被回收掉了。
通过模拟entry的对照实验,可以发现entry里key时弱引用,gc时会回收,value是强引用,不会被回收掉。这也是一些threadlocal没有remove导致内存泄漏的原罪。
用一副内存结构图来表示一下ThreadLocal
结论:
1.ThreadLocal用完要remove清理掉,防止出现其他问题。
2.WeakReferene