ThreadLocal原理及内存泄漏问题

ThreadLocal的作用及使用场景

作用

ThreadLocal为每一个线程保存独立的副本信息,这样每个线程都可以修改和使用自己的副本对象,做到了线程隔离,不会影响其他线程的副本使用,确保了线程安全。

场景

最典型的就是使用SimpleDateFormat类的时候,会把SimpleDateFormat储存到ThreadLocal中做线程隔离,因为SimpleDateFormat是线程不安全的,避免了频繁地去创建对象。

关于SimpleDateFormat类线程不安全的问题可以看我的另外一篇文章:

为什么SimpleDateFormat要放到ThreadLocal里?_王忠的博客-CSDN博客

Thread、ThreadLocal、ThreadLocalMap关系

public class Thread implements Runnable {    
    ...
    ThreadLocal.ThreadLocalMap threadLocals = null;
 
    ...
}

一个线程里,维护了一个成员变量ThreadLocalMap。

 public class ThreadLocal<T> {
    ...
    static class ThreadLocalMap {
 
        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
 
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
            ...
            private Entry[] table;
        }
    }

ThreadLocalMap为ThreadLocal的一个静态内部类,他的key为你创建的ThreadLocal对象,且是弱引用。他的value即为你想要保存的副本对象。ThreadLocalMap里可以保存多对ThreadLocal和副本对象。

ThreadLocal内存泄漏

ThreadLocal应用示意图
ThreadLocal引用示意图

 这张图老生常谈了,讲内存泄漏问题时的必需图。

上面讲了Thread、ThreadLocal、ThreadLocalMap之间的关系,再根据这张图来看,当我们把创建的ThreadLocal对象置为null的时候,ThreadLocal这个key在下次gc的时候就会被回收;此时,对应的value因为存在一条引用链路,符合可达性分析,不会被回收掉,但同时已经无法访问到该value了,所以造成了内存泄漏。

如果使用完之后线程就结束销毁了那倒没啥事,但是很多场景我们是放在线程池里去用的,线程会保持存在的状态,那就会有内存泄漏的问题。

解决内存泄漏的方法

源码考虑到这种脏entry(key为null)的问题,所以在set、getEntry、remove等方法里都对脏entry做了清除,有兴趣的可以自己去看下源码。

工作中最常见的避免内存泄漏的方式就是在finally代码块里进行remove操作。

ThreadLocal类的使用

initialValue

protected T initialValue() {
   return null;
}

如果不想用set方法,调用get方法时直接能进行初始化,就要重写initialValue方法,在方法里进行副本对象的初始化。

该方法返回当前线程局部变量的初始化值。当第一次使用get方法访问变量时调用该方法,但若该线程在这之前调用了set方法,则不会调用initialValue方法。正常情况下,该方法最多被调用一次,但是如果后续调 用了remove,然后再调用get,则该方法可能再次被调用。

private static final ThreadLocal<Integer> threadId =
                new ThreadLocal<Integer>() {
                    @Override
                    protected Integer initialValue() {
                        return nextId.getAndIncrement();
                    }
                };

withInitial

java1.8之后的版本,idea会更推荐使用withInitial方法来进行初始化操作,而非initialValue。

private static final ThreadLocal<Map<String, Object>> mapLocal = 
    ThreadLocal.withInitial(HashMap::new);

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值