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的引用

题外话 WeakReference也是有很多知识点的,这个可以看java 内存回收和四种引用关系_SoftReference,WeakReference,PhantomReference
现在 ,只需要知道
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 我们更应该将他看成是 工具。

  • 6
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值