基于ThreadLocal 的用法及内存泄露(内存溢出)

目录
  • 使用
  • 构造方法
    • 静态方法
    • 公共方法
  • 内存泄露
  • 解决方法
    • 为什么要将ThreadLocal 定义成 static 变量
  • 对ThreadLocal内存泄漏引起的思考
    • 概述
      • 使用场景样例代码
      • ThreadLocal使用源码
      • 思考问题
    • ThreadLocal解读

ThreadLocal 看名字 就可以看出一点头绪来,线程本地。

来看一下java对他的描述:

该类提供线程本地变量。这些变量与它们的正常对应变量的不同之处在于,每个线程(通过ThreadLocal的 get 或 set方法)访问自己的、独立初始化的变量副本。 ThreadLocal实例通常是类中的私有静态字段。

上面这段话呢,一个重点就是 每个线程都有自己的专属变量,这个专属变量呢,是不会被其他线程影响的。

使用

public class ThreadLocalTwo {
    //静态的 延长生命周期。final  不可改变
    private static final ThreadLocal<Integer> threalLocal = ThreadLocal.withInitial(() -> {
        return 0;
    });
    public static void main(String[] args) {
        new Thread(() -> {
            while (true) {
                //取出来
                int inner = threalLocal.get();
                //使用
                System.out.println(Thread.currentThread().getName() + "   " + inner);
                LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
                //更新值存入
                threalLocal.set(++inner);
            }
        }, "three").start();
        new Thread(() -> {
            while (true) {
                //取出来
                int inner = threalLocal.get();
                //使用
                System.out.println(Thread.currentThread().getName() + "   " + inner);
                LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
                //更新值存入
                threalLocal.set(++inner);
            }
        }, "four").start();
    }
}

使用这个我只是随便写一个demo,具体的逻辑有很多种,只要你想,就会有很多种写法。具体看业务需求。

个人理解

ThreadLocal 类似于一个工具,通过这个工具,来为当前线程设定修改移除本地副本。,如果 你查看Thread的源码会发现下面这段代码

    /* ThreadLocal values pertaining to this thread. This map is maintained
     by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

这是静态内部类构造的一个字段,那么我们看一下 ThreadLocal.ThreadLocalMap的源码.

     static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

上面代码我们可以发现 ThreadLocal.ThreadLocalMap这个内部静态类,里面还包含这一个内部静态类Entry。

这个Entry 继承了WeakReference,并且将ThreadLocal作为弱引用类型。这表明 ThreadLocal如果没有其他的强引用时候,说不定 有可能不知道啥时候就被回收了。

那么至于 value呢? 我可以肯定的告诉你 value不会被回收,即便 传进来的v是个匿名类。

value持有着线程的本地副本的引用

Entry[] table 这个持有 entry的引用

现在 ,只需要知道

1 弱引用对象,会持有引用对象的引用,弱引用对象并不能决定 引用对象是否回收。

2 弱引用的子类的 如果有自己的字段的话, 那么那个字段是强引用,不会被回收

3 弱引用对象,如果是new出来的,那么弱引用对象本身也是一个强引用。弱引用对象自己不会被回收。

构造方法

一个默认的无参构造方法 ,没啥好讲的,,

public ThreadLocal() {
    }

使用

  private static final ThreadLocal<String> construct  = new ThreadLocal<>(){
        //如果 不重写这个方法的话,默认返回null
        @Override
        protected String initialValue() {
            return "默认值";
        }
    };

静态方法

note Java8新增的方法

 public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }

上面的这个静态方法呢,生成一个ThreadLocal对象,参数是一个Supplier函数接口。

下面展示一个代码

private static final ThreadLocal<String> local = ThreadLocal.withInitial(() -> "默认值");

上面这段代码使用了Lambda表达式, 比起上面 new 并且重写方法的写法,代码会少很多,显得很有逼格对不。

如果你对java8的Lambda不清楚的话,可以看这篇文章:java Lambda表达式的使用

公共方法

//返回当前线程本地副本的值。如果本地副本为null,则返回初始化为调用{@link #initialValue}方法返回的值。
public T get()
//将当前线程的本地副本 设为 value
public void set(T value)
//将当前线程的本地副本移除,如果后面调用get()方法的话,会返回T initialValue()的值
public void remove()

内存泄露

接下来讲一下,ThreadLocal配合线程池时候 会出现内存泄漏的原理。按照我的个人理解 ,是因为内存溢出造成的。内存泄露指的是 原本应该回收的对象,现在由于种种原因,无法被回收。

为什么上面会强调 配合线程池的时候,因为单独线程的时候,当线程任务运行完以后,线程资源会被回收,自然 本地副本也被回收了。而线程池里面的线程不全被回收(有的不会被回收,也有的会被回收)。

现在来看一下上面的Entry这个最终存储本地副本的静态内部类,

   static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

下面内容需要你对 java 内存管理关系了解,否则 你肯定会一脸蒙蔽。

如果 你不会 可以看我这篇文章java内存管理关系及内存泄露的原理

由于它是WeakReference的子类,所以 作为引用对象的 ThreadLocal,就有可能会被Entry清除引用。如果这时候 ThreadLocal没有其他的引用,那么它肯定就会被GC回收了。

但是value 是强引用,而Entry 又被Entry[]持有,Entry[]又被ThreadLocalMap持有,ThreadLocalMap又被线程持有。只要线程不死或者 你不调用set,remove这两个方法之中任何一个,那么value指向的这个对象就始终 不会被回收。因为 不符合GC回收的两个条件的任何一个。

试想一下如果线程池里面的线程足够的多,并且 你传给线程的本地副本内存占用又很大。毫无疑问 会内存溢出。

解决方法

只要调用remove 这个方法会擦出 上一个value的引用,这样线程就不会持有上一个value指向对象的引用。就不会有内存露出了。

有读者会有疑问了,上面不是说两个放过会使value对象可以回收么,怎么上面没有set方法呢?

这个是因为,set方法确实可以是value指向的对象 这个引用断开,但同时它又强引用了一个内存空间给value。即使上一个对象被回收了,但是新对象也产生了。

至于 get方法,只有在ThreadLocalMap 被GC后,调用get方法 才会将value对应的引用切断。

首先,我们看get源码

  public T get() {
        Thread t = Thread.currentThread();//当前线程的引用
        //得到当前线程的ThreadLocalMap,如果没有返回null
        ThreadLocalMap map = getMap(t);
        //存在时候走这个
        if (map != null) {
             //与键关联的项,如果没有键则为null
             //如果ThreadLocalMap的entry 清除了ThreadLocal 对象的引用,那么这个会清除对应的value 引用
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //当前线程 没有设置ThreadLocalMap,那么返回initialValue()的值
        return setInitialValue();
    }

上面这段代码,调用了getEntry,这个方法内部调用了 另一个方法,实现了当ThreadLocal被清除引用后,也清除对应的value引用,

    private Entry getEntry(ThreadLocal<?> key) {
            //得到位置  table数组 的容量是16
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
             //key没有被回收后
            if (e != null && e.get() == key)
                return e;
            else
                 //这个key被回收 调用,将对应的value 释放引用
                return getEntryAfterMiss(key, i, e);
        }

我们看见最后调用 getEntryAfterMiss(key, i, e),这个方法 也不是最终的擦除value引用的方法,我们接着往下看

 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
            while (e != null) {
                 //得到弱引用对象 持有的引用对象的引用
                ThreadLocal<?> k = e.get();
                //ThreadLocal没有被回收
                if (k == key)
                    return e;

                if (k == null)
                    //entry 清除ThreadLocal的引用
                   //通过entry[]数组的元素entry 清除entry的value引用
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

这上面呢,我们要关注expungeStaleEntry(i),这个才是最终的擦除entry的value对象的引用。 看一下 expungeStaleEntry(i)的源码

 private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;//得到table引用
            int len = tab.length;//得到table的长度,不出意外 应该是16
            // expunge entry at staleSlot
           //下面两句代码 是关键。
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;
            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;
                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

上面这段代码很长,我们不必细看个,关注下面这两行代码就行

            tab[staleSlot].value = null;//清除引用  这样 GC就可以回收了
            tab[staleSlot] = null;//清除自身的引用

通过entry[staleSlot]得到存储的entry ,通过entry清除entry的value引用。

这样大家明白了吧,get也是可以起到和remove一样的效果的。

我们再看一下remove的源码

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

上面这段代码没什么说的,直接看ThreadLocalMap的remove方法

    private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            //得到位置,因为存的时候 也是按照这个规则来的,
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                 //这里有可能会发生 ThreadLocal 被entry清除引用,那么value就被线程引用了,如果不调用set,get方法的话,只能等待线程销毁。
                if (e.get() == key) {
                    //调用弱引用的方法 , 将引用对象的引用清除
                    e.clear();
                    //擦出ThreadLocal 对应的value
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

上面调用了 expungeStaleEntry 擦除。

set

我们关注这个方法

  private void expungeStaleEntries() {
            Entry[] tab = table;
            int len = tab.length;
            for (int j = 0; j < len; j++) {
                Entry e = tab[j];
                if (e != null && e.get() == null)
                    //擦除
                    expungeStaleEntry(j);
            }
        }

这个呢 循环调用了expungeStaleEntry(j)方法 ,也是擦除了value的对象引用。

为什么要将ThreadLocal 定义成 static 变量

延长生命周期,之所以是static 是因为,ThreadLocal 我们更应该将他看成是 工具。

对ThreadLocal内存泄漏引起的思考

概述

最近在对一个项目进行重构,用到了ThreadLocal。

场景如下:

外围系统会调用接口上传数据,在接口中要记录数据的变化Id,在上传数据完后需要集中在一个地方把这些Id以消息形式发送出去。

使用场景样例代码

    public Result<Void> uploadOrder(TotalPayInfoVo totalPayInfoVo) {
        try {
            saveTotalPayInfoVo(totalPayInfoVo);
            //发送消息
            UnitWork.getCurrent().pushMessage();
        } catch (Exception e) {
            cashLogger.error("uploadOrder error,data: {}, error: {}", JSON.toJSONString(totalPayInfoVo), e);
            throw new RuntimeException("保存失败", e);
        } finally {
            UnitWork.clean();//
        }
        return ResultUtil.successResult();避免内存泄漏
    }

ThreadLocal使用源码

/**
 * 工作单元,在同一个线程中负责记录一个事件或者一个方法或者一个事务过程中产生的变化,等操作结束后再处理这种变化。
 */
public class UnitWork {
    private UnitWork() {
    }
    private static ThreadLocal<UnitWork> current = new ThreadLocal<UnitWork>() {
        protected UnitWork initialValue() {
            return new UnitWork();
        }
    };
    /**
     * 状态变化的instance
     */
    private Set<String> statusChangedInstances = new HashSet<>();
    public void addStatusChangedInstance(String instance) {
        statusChangedInstances.add(instance);
    }
    /**
     * 推送消息
     */
    public void pushMessage() {
       for(String id : statusChangedInstances){
            //异步发消息
       }
    }
    public static UnitWork getCurrent() {
        return current.get();
    }
    /**
     * 删除当前线程的工作单元,建议放在finally中调用,避免内存泄漏
     */
    public static void clean() {
        current.remove();
    }
}

思考问题

为了避免内存泄漏,每次用完做一下clean清理操作。发送消息的过程是异步的,意味着clean的时候可能和发送消息同时进行。那么会不会把这些Id清理掉?那么可能造成消息发送少了。要回答这个问题,首先要搞懂ThreadLocal的引用关系,remove操作做了什么?

ThreadLocal解读

ThreadLocal可以分别在各个线程保存变量独立副本。每个线程都有ThreadLocalMap,顾名思义,类似Map容器,不过是用数组Entry[]来模拟的。那么既然类似Map,肯定会存在Key。其实Key是ThreadLocal类型,Key的值是ThreadLocal的HashCode,即通过threadLocalHashCode计算出来的值。

这个Map的Entry并不是ThreadLocal,而是一个带有弱引用的Entry。既然是弱引用,每次GC的时候都会回收。

        static class Entry extends WeakReference<ThreadLocal> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }

而Key对应的value就是要保存在线程副本Object,这里指的就是UnitWork的实例。调用ThreadLocal的get方法时,首先找到当前线程的ThreadLocalMap,然后根据这个ThreadLocal算出来的hashCode找到保存线程副本Object。

他们的关系对应如下:

ThreadLocal在remove的时候,会调用Entry的clear,即弱引用的clear方法。把Key->ThreadLocal的引用去掉。接下来的expungeStaleEntry会把entry中value引用设置为null。

        /**
         * Remove the entry for key.
         */
        private void remove(ThreadLocal key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

现在可以回答之前提前的问题。虽然ThreadLocal和当前线程都会与Object脱离了引用的关系,但是最重要一点就是异步的线程仍然存在一条强引用路径到Object,即到UnitWork实例的强引用。因此GC然后不会回收UnitWork的实例,发消息还是不会少发或者出现空指针情况。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • ThreadLocal原理及内存泄漏原因

    ThreadLocal有两个问题: 1. 每个变量副本是存储在哪了? 2. 变量副本是怎样从共享的变量中赋值出来的?源码中threadlocal的初始值是什么时候设置的. ThreadLocal为每个线程维护一个变量的副本? 每个线程的ThreadLocalMap都是线程自身持有的,但是初始化是在ThreadLocal中,然后每个线程相当于保存了一个map 这个map存的key是LocalThread的实例,value是存储的线程的局部变量 get方法 根据当前线程获取Thread中的值 set

  • 实例详解Java中ThreadLocal内存泄露

    案例与分析 问题背景 在 Tomcat 中,下面的代码都在 webapp 内,会导致WebappClassLoader泄漏,无法被回收. public class MyCounter { private int count = 0; public void increment() { count++; } public int getCount() { return count; } } public class MyThreadLocal extends ThreadLocal<MyCount

  • java线程本地变量ThreadLocal详解

    介绍 ThreadLocal作为JDK1.2以来的一个java.lang包下的一个类,在面试和工程中都非常重要,这个类的主要目的是提供线程本地的变量,所以也有很多地方把这个类叫做线程本地变量 从字面理解,这个类为每个线程都创建了一个本地变量,实际上是ThreadLocal为变量在每个线程中都创建了一个副本,使得每个线程都可以访问自己内部的副本变量 通常提到多线程,都会考虑变量同步的问题,但是ThreadLocal并不是为了解决多线程共享变量同步的问题,而是为了让每个线程的变量不互相影响,相当于线

  • ThreadLocal内存泄漏问题解决方案

    如果说 ThreadLocal 的话,那肯定就会涉及到内存泄漏,为啥嘞 因为 吧啦吧啦 ~ ThreadLocal 解决了什么问题呢? 它是为了解决对象不能被多线程共享访问的问题,通过 threadLocal.set() 方法将对象实例保存在每个线程自己所拥有的 threadLocalMap 中,这样的话每个线程都使用自己的对象实例,彼此不会影响从而达到了隔离的作用,这样就解决了对象在被共享访问时带来的线程安全问题. 啥意思呢?打个比方,现在公司所有人都要填写一个表格,但是只有一支笔,这个时候就

  • 基于ThreadLocal 的用法及内存泄露(内存溢出)

    目录 使用 构造方法 静态方法 公共方法 内存泄露 解决方法 为什么要将ThreadLocal 定义成 static 变量 对ThreadLocal内存泄漏引起的思考 概述 使用场景样例代码 ThreadLocal使用源码 思考问题 ThreadLocal解读 ThreadLocal 看名字 就可以看出一点头绪来,线程本地. 来看一下java对他的描述: 该类提供线程本地变量.这些变量与它们的正常对应变量的不同之处在于,每个线程(通过ThreadLocal的 get 或 set方法)访问自己的.

  • IE8 内存泄露(内存一直增长 )的原因及解决办法

    最近开发的时候对页面使用了定时的局部更新,结果在ie6,7和Firefox下,一切正常,而在ie8下过上几个小时就浏览器就崩溃了,显示是内存溢出,我以为是代码写的不好导致内存泄露,但是ie6,7又正常,调查了一下,原来这是ie8的bug. 问题点 在IE8中,生成特定Dom节点所占用的内存是不会被释放的,即使这些节点被删除内存也不会被释放. 内存泄露的节点类型包括:form.button.input.select.textarea.a.img和objec 其他的大部分节点类型是不会泄露的,例如:

  • 浅谈Java编程中的内存泄露情况

    必须先要了解的 1.c/c++是程序员自己管理内存,Java内存是由GC自动回收的. 我虽然不是很熟悉C++,不过这个应该没有犯常识性错误吧. 2.什么是内存泄露? 内存泄露是指系统中存在无法回收的内存,有时候会造成内存不足或系统崩溃. 在C/C++中分配了内存不释放的情况就是内存泄露. 3.Java存在内存泄露 我们必须先承认这个,才可以接着讨论.虽然Java存在内存泄露,但是基本上不用很关心它,特别是那些对代码本身就不讲究的就更不要去关心这个了. Java中的内存泄露当然是指:存在无用但是垃

  • Java基础之内存泄漏与溢出详解

    一.浅析 内存泄露( memory leak):是指程序在申请内存后,无法释放已申请的内存空间,多次内存泄露堆积后果很严重,内存迟早会被占光.内存泄漏最终会造成内存溢出. 内存溢出(out of memory) :是指程序在申请内存时,没有足够的内存空间供其使用 JVM中有一下几种内存空间: 栈内存(Stack):每个线程私有的. 堆内存(Heap):所有线程公用的. 方法区(Method Area):有点像以前常说的"进程代码段",这里面存放了每个加载类的反射信息.类函数的代码.编译

  • 面试官:java ThreadLocal真的会造成内存泄露吗

    目录 1.ThreadLocal知识体系 2.为什么会被设计为弱引用呢? 3.大量Entry造成的内存溢出问题探讨 总结 1.ThreadLocal知识体系 本文还是不能免俗,在回答这个问题之前需要先和大家介绍一下ThreadLocal的知识,使大家对ThreadLocal有一个相对全面的认识. ThreadLocal本地线程变量,主要用于解决数据访问的竞争,通常用于多租户.全链路压测.链路跟踪中保存线程上下文环境,在一个请求流转中非常方便的获取一些关键信息,例如当前的租户信息.压测标记. Th

  • ThreadLocal作用原理与内存泄露示例解析

    目录 ThreadLocal作用 简单例子 局部变量.成员变量 . ThreadLocal.静态变量 共享 or 隔离 原理 源码分析 TheadLocal TheadLocalMap ThreadLocal与内存泄漏 小结 ThreadLocal作用 对于Android程序员来说,很多人都是在学习消息机制时候了解到ThreadLocal这个东西的.那它有什么作用呢?官方文档大致是这么描述的: ThreadLocal提供了线程局部变量 每个线程都拥有自己的变量副本,可以通过ThreadLocal

  • 详解基于node的前端项目编译时内存溢出问题

    前段时间公司有个基于vue的项目在运行npm run build的时候会报内存溢出,今天在某个技术流交群也有位小伙伴基于angular的项目也出现了这个问题,所以查了一些相关的资料总结了一下,下面会详细说明前端三大框架编译时遇到这个问题具体怎么解决.首先看我模拟出的报错内容 具体截图如下 里面有句关键的话,CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory JavaScript堆内存不足,这里说的 JavaS

  • JS闭包、作用域链、垃圾回收、内存泄露相关知识小结

    补充: 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现. 闭包的特性 闭包有三个特性: 1.函数嵌套函数 2.函数内部可以引用外部的参数和变量 3.参数和变量不会被垃圾回收机制回收 闭包的定义及其优缺点 闭包 是指有权访问另一个函数作用域中的变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量 闭包的缺点就是常驻内存,会增大内存使用量,使用不当很容易造成内存泄露. 闭包是javascript

  • Android 优化Handler防止内存泄露

    Android 优化Handler防止内存泄露 Demo描述: Handler可能导致的内存泄露及其优化 1 关于常见的Handler的用法但是可能导致内存泄露 2 优化方式请参考BetterHandler和BetterRunnable的实现 package cc.cc; import java.lang.ref.WeakReference; import android.os.Bundle; import android.os.Handler; import android.os.Messag

  • android的GC内存泄露问题

    1. android内存泄露概念 不少人认为JAVA程序,因为有垃圾回收机制,应该没有内存泄露.其实如果我们一个程序中,已经不再使用某个对象,但是因为仍然有引用指向它,垃圾回收器就无法回收它,当然该对象占用的内存就无法被使用,这就造成了内存泄露.如果我们的java运行很久,而这种内存泄露不断的发生,最后就没内存可用了.当然java的,内存泄漏和C/C++是不一样的.如果java程序完全结束后,它所有的对象就都不可达了,系统就可以对他们进行垃圾回收,它的内存泄露仅仅限于它本身,而不会影响整个系统的

随机推荐