拨开荷叶行,寻梦已然成。仙女莲花里,翩翩白鹭情。
IMG-LOGO
主页 文章列表 全方位、多角度理解 ThreadLocal,还有谁不会??

全方位、多角度理解 ThreadLocal,还有谁不会??

白鹭 - 2022-02-02 2115 0 0

来源: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 评论

发表评论

您的电子邮件地址不会被公开。 必填的字段已做标记 *