ThreadLocal没用好的例子(构造用户上下文)

问题:

讲述一个以前遇到的问题,问题的现象是这样的,通过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原理

介绍一下ThreadLocal原理,增加理解。

Thread

每个Thread对象中都保存着一个ThreadLocal.ThreadLocalMap类型的变量threadLocals,调用ThreadLocal实例的set方法时会将ThreadLocal这个实例,和set入参当作键值对存储到当前调用线程的threadLocals中。

ThreadLocalMap

类似于map,存储Threadlocal实例作为弱引用key,object对象作为value。key是弱引用,弱引用是一旦发生垃圾收集行为,不管内存够不够,都会进行收集,也就是说GC后,key如果没有其他变量的强引用,key就消失了,但是value是强引用,一直在内存中。

ThreadLocal没用好的例子(构造用户上下文)_第1张图片

ThreadLocal

几个重要的方法

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);
     }

弱(WeakReference)引用实验

对照实验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();
        }
    }

ThreadLocal没用好的例子(构造用户上下文)_第2张图片

对照实验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();
        }
    }

ThreadLocal没用好的例子(构造用户上下文)_第3张图片 

对照实验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();
        }
    }

ThreadLocal没用好的例子(构造用户上下文)_第4张图片 

模拟ThreadLocalMap中entry的实验

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的时候不会清除掉 ThreadLocal没用好的例子(构造用户上下文)_第5张图片

对照实验二:

将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被回收掉了。

ThreadLocal没用好的例子(构造用户上下文)_第6张图片 通过模拟entry的对照实验,可以发现entry里key时弱引用,gc时会回收,value是强引用,不会被回收掉。这也是一些threadlocal没有remove导致内存泄漏的原罪。

用一副内存结构图来表示一下ThreadLocal

ThreadLocal没用好的例子(构造用户上下文)_第7张图片

 

结论:

1.ThreadLocal用完要remove清理掉,防止出现其他问题。

2.WeakReferene是弱引用,如果WeakReferene引用的实例没有其他对象引用连接,GC的时候会被清理掉,只清理范型的那个值。

 

 

你可能感兴趣的:(java,开发语言)