起源: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开发手册(嵩山版)》最新公布,速速下载!

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