关于java:全方位多角度理解-ThreadLocal还有谁不会

3次阅读

共计 4464 个字符,预计需要花费 12 分钟才能阅读完成。

起源:blog.csdn.net/zzg1229059735/article/details/82715741

本次给大家介绍重要的工具 ThreadLocal。解说内容如下,同时介绍什么场景下产生内存透露,如何复现内存透露,如何正确应用它来防止内存透露。

  • ThreadLocal 是什么?有哪些用处?
  • ThreadLocal 如何应用
  • ThreadLocal 原理
  • ThreadLocal 应用有哪些坑及注意事项

1. ThreadLocal 是什么?有哪些用处?

首先介绍 Thread 类中属性 threadLocals:

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

咱们发现 Thread 并没有提供成员变量 threadLocals 的设置与拜访的办法,那么每个线程的实例 threadLocals 参数咱们如何操作呢?这时咱们的配角:ThreadLocal 就退场了。

所以有那么一句总结:ThreadLocal 是线程 Thread 中属性 threadLocals 的管理者。

也就是说咱们对于 ThreadLocal 的 get, set,remove 的操作后果都是针对以后线程 Thread 实例的 threadLocals 存,取,删除操作。相似于一个开发者的工作,产品经理左右不了,产品经理只能通过技术 leader 来给开发者分配任务。上面再举个栗子,进一步阐明他们之间的关系:

  1. 每个人都一张银行卡
  2. 每个人每张卡都有肯定的余额。
  3. 每个人获取银行卡余额都必须通过该银行的管理系统。
  4. 每个人都只能获取本人卡持有的余额信息,别人的不可拜访。

映射到咱们要说的 ThreadLocal

  1. card 相似于 Thread
  2. card 余额属性,卡号属性等相似于 Treadlocal 外部属性汇合 threadLocals
  3. cardManager 相似于 ThreadLocal 治理类

那 ThreadLocal 有哪些利用场景呢?

其实咱们无意间曾经时时刻刻在应用 ThreadLocal 提供的便当,如果说多数据源的切换你比拟生疏,那么 spring 提供的申明式事务就再相熟不过了,咱们在研发过程中无时无刻不在应用,而 spring 申明式事务的重要实现根底就是 ThreadLocal,只不过大家没有去深入研究 spring 申明式事务的实现机制。前面有机会我会给大家介绍 spring 申明式事务的原理及实现机制。

原来 ThreadLocal 这么弱小,但利用开发者应用较少,同时有些研发人员对于 ThreadLocal 内存透露,等潜在问题,不敢试用,恐怕这是对于 ThreadLocal 最大的误会,前面咱们将会仔细分析,只有依照正确应用形式,就没什么问题。如果 ThreadLocal 存在问题,岂不是 spring 申明式事务是咱们程序最大的潜在危险吗?

2.ThreadLocal 如何应用

为了更直观的领会 ThreadLocal 的应用咱们假如如下场景

  1. 咱们给每个线程生成一个 ID。
  2. 一旦设置,线程生命周期内不可变动。
  3. 容器流动期间不能够生成反复的 ID

咱们创立一个 ThreadLocal 治理类:

测试程序如下:咱们同一个线程一直 get,测试 id 是否变动,同时测试实现后咱们就将其开释掉。

在主程序中咱们开启多个线程测试不通线程之间是否会影响

不出意外咱们的后果为:

后果:的确是不同线程间 id 不同,雷同线程 id 雷同。

3.ThreadLocal 原理

①ThreadLocal 类构造及办法解析:

上图可知:ThreadLocal 三个办法 get, set , remove 以及外部类 ThreadLocalMap

②ThreadLocal 及 Thread 之间的关系:

从这张图咱们能够直观的看到 Thread 中属性 threadLocals,作为一个非凡的 Map,它的 key 值就是咱们 ThreadLocal 实例,而 value 值这是咱们设置的值。

③ThreadLocal 的操作过程:

咱们以 get 办法为例:

其中 getMap(t) 返回的就上以后线程的 threadlocals,如下图,而后依据以后 ThreadLocal 实例对象作为 key 获取 ThreadLocalMap 中的 value,如果首次进来这调用 setInitialValue()

set 的过程也相似:

留神:ThreadLocal 中能够间接 t.threadLocals 是因为 Thread 与 ThreadLocal 在同一个包下,同样 Thread 能够间接拜访 ThreadLocal.ThreadLocalMap threadLocals = null; 来进行申明属性。

4.ThreadLocal 应用有哪些坑及注意事项

我常常在网上看到骇人听闻的题目,ThreadLocal 导致内存透露,这通常让一些刚开始对 ThreadLocal 了解不透彻的开发者,不敢贸然应用。越不必,越生疏。这样就让咱们错失了更好的实现计划,所以敢于引入新技术,敢于踩坑,能力不断进步。

咱们来看下为什么说 ThreadLocal 会引起内存透露,什么场景下会导致内存透露?

先回顾下什么叫内存透露,对应的什么叫内存溢出

  • ①Memory overflow: 内存溢出,没有足够的内存提供申请者应用。
  • ②Memory leak: 内存透露,程序申请内存后,无奈开释已申请的内存空间,内存透露的沉积终将导致内存溢出。

显然是 TreadLocal 在不标准应用的状况下导致了内存没有开释。

红框里咱们看到了一个非凡的类 WeakReference,同样这个类,利用开发者也同样很少应用,这里简略介绍下吧

既然 WeakReference 在下一次 gc 行将被回收,那么咱们的程序为什么没有出问题呢?

①所以咱们测试下弱援用的回收机制:

这一种存在强援用不会被回收。

这里没有强援用将会被回收。

下面演示了弱援用的回收状况,上面咱们看下 ThreadLocal 的弱援用回收状况。

②ThreadLocal 的弱援用回收状况

如上图所示,咱们在作为 key 的 ThreadLocal 对象没有内部强援用,下一次 gc 必将产生 key 值为 null 的数据,若线程没有及时完结必然呈现,一条强援用链 Threadref–>Thread–>ThreadLocalMap–>Entry,所以这将导致内存透露。

上面咱们模仿复现 ThreadLocal 导致内存透露:

1. 为了成果更佳显著咱们将咱们的 treadlocals 的存储值 value 设置为 1 万字符串的列表:

class ThreadLocalMemory {
    // Thread local variable containing each thread's ID
    public ThreadLocal<List<Object>> threadId = new ThreadLocal<List<Object>>() {
        @Override
        protected List<Object> initialValue() {List<Object> list = new ArrayList<Object>();
            for (int i = 0; i < 10000; i++) {list.add(String.valueOf(i));
            }
            return list;
        }
    };
    // Returns the current thread's unique ID, assigning it if necessary
    public List<Object> get() {return threadId.get();
    }
    // remove currentid
    public void remove() {threadId.remove();
    }
}

测试代码如下:

public static void main(String[] args)
            throws InterruptedException {

        //  为了复现 key 被回收的场景,咱们应用长期变量
        ThreadLocalMemory memeory = new ThreadLocalMemory();

        // 调用
        incrementSameThreadId(memeory);

        System.out.println("GC 前:key:" + memeory.threadId);
        System.out.println("GC 前:value-size:" + refelectThreadLocals(Thread.currentThread()));

        // 设置为 null,调用 gc 并不一定触发垃圾回收,然而能够通过 java 提供的一些工具进行手工触发 gc 回收。memeory.threadId = null;
        System.gc();

        System.out.println("GC 后:key:" + memeory.threadId);
        System.out.println("GC 后:value-size:" + refelectThreadLocals(Thread.currentThread()));

        // 模仿线程始终运行
        while (true) {}}

此时咱们如何晓得内存中存在 memory leak 呢?

咱们能够借助 jdk 提供的一些命令 dump 以后堆内存,命令如下:

jmap -dump:live,format=b,file=heap.bin <pid>

而后咱们借助 MAT 可视化剖析工具,来查看对内存,剖析对象实例的存活状态:

首先关上咱们工具提醒咱们的内存透露剖析:

这里咱们能够确定的是 ThreadLocalMap 实例的 Entry.value 是没有被回收的。

最初咱们要确定 Entry.key 是否还在?关上 Dominator Tree,搜寻咱们的 ThreadLocalMemory,发现并没有存活的实例。

以上咱们复现了 ThreadLocal 不正当应用,引起的内存透露。demo 在这里。

所以咱们总结了应用 ThreadLocal 时会产生内存透露的前提条件:

  • ①ThreadLocal 援用被设置为 null,且前面没有 set,get,remove 操作。
  • ②线程始终运行,不进行。(线程池)
  • ③触发了垃圾回收。(Minor GC 或 Full GC)

咱们看到 ThreadLocal 呈现内存透露条件还是很刻薄的,所以咱们只有毁坏其中一个条件就能够防止内存透露,单但为了更好的防止这种状况的产生咱们应用 ThreadLocal 时恪守以下两个小准则:

  • ①ThreadLocal 申明为 private static final。
    • Private 与 final 尽可能不让别人批改变更援用,
    • Static 示意为类属性,只有在程序完结才会被回收。
  • ②ThreadLocal 应用后务必调用 remove 办法。
    • 最简略无效的办法是应用后将其移除。

以上。

近期热文举荐:

1.1,000+ 道 Java 面试题及答案整顿 (2022 最新版)

2. 劲爆!Java 协程要来了。。。

3.Spring Boot 2.x 教程,太全了!

4.Spring Boot 2.6 正式公布,一大波新个性。。

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

正文完
 0