ThreadLocal原理介绍及应用场景

本次给大家介绍重要的工具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是因为ThreadThreadLocal在同一个包下,同样Thread可以直接访问ThreadLocal.ThreadLocalMap threadLocals = null;来进行声明属性。

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

我经常在网上看到骇人听闻的标题,ThreadLocal导致内存泄漏,这通常让一些刚开始对ThreadLocal理解不透彻的开发者,不敢贸然使用。越不用,越陌生。这样就让我们错失了更好的实现方案,所以敢于引入新技术,敢于踩坑,才能不断进步。

我们来看下为什么说ThreadLocal会引起内存泄漏,什么场景下会导致内存泄漏?

先回顾下什么叫内存泄漏,对应的什么叫内存溢出

  • ①Memory overflow:内存溢出,没有足够的内存提供申请者使用。
  • ②Memory leak:内存泄漏,程序申请内存后,无法释放已申请的内存空间,内存泄漏的堆积终将导致内存溢出。

显然是TreadLocal在不规范使用的情况下导致了内存没有释放。

红框里我们看到了一个特殊的类WeakReference,同样这个类,应用开发者也同样很少使用,这里简单介绍下吧

类型 回收时间 应用场景
强引用 一直存活,除非GC Roots不可达 所有程序的场景,基本对象,自定义对象等
软引用 内存不足时会被回收 一般用在对内存非常敏感的资源上,用作缓存的场景比较多,例如:网页缓存、图片缓存
弱引用 只能存活到下一次GC前 生命周期很短的对象,例如ThreadLocal中的Key。
虚引用 随时会被回收, 创建了可能很快就会被回收 可能被JVM团队内部用来跟踪JVM的垃圾回收活动

既然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方法。
    最简单有效的方法是使用后将其移除。

到此这篇关于ThreadLocal原理介绍及应用场景的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Java并发编程之threadLocal

    目录 1.ThreadLocal介绍 2.ThreadLocal使用实例 3.ThreadLocal实现原理 1.ThreadLocal介绍 多个线程访问同一个共享变量时特别容易出现并发问题,特别是多线程需要对共享变量进行写入时.为了保证线程安全,一般使用者在访问共享变量的时候需要进行适当的同步,如图 同步的一般措施是加锁,这就需要使用者对锁有一定的了解,这显然加重了使用者的负担,那么有没有一种方法可以做到,当创建一个变量后,每个线程对其进行访问的时候访问的是自己线程的变量呢?其实ThreadL

  • Java并发编程之ThreadLocal详解

    目录 一.什么是ThreadLocal? 二.ThreadLocal的使用场景 三.如何使用ThreadLocal 四.数据库连接时的使用 五.ThreadLocal工作原理 六.小结 七.注意点 一.什么是ThreadLocal? ThreadLocal叫做线程本地变量,ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的.ThreadLocal为变量在每个线程中都创建了一个副本,则每个线程都可以访问自己内部的副本变量. 二.ThreadLocal的使用场景 1.当对象

  • Java中的ThreadLocal详解

    目录 一.ThreadLocal简介 二.ThreadLocal简单使用 三.ThreadLocal的实现原理 1.set方法源码 2.get方法源码 3.remove方法的实现 四.ThreadLocal不支持继承性 五.InheritableThreadLocal类 六.从ThreadLocalMap看ThreadLocal使用不当的内存泄漏问题 1.基础概念 2.分析ThreadLocalMap内部实现 一.ThreadLocal简介 多线程访问同一个共享变量的时候容易出现并发问题,特别是

  • Java多线程 ThreadLocal原理解析

    目录 1.什么是ThreadLocal变量 2.ThreadLocal实现原理 3.内存泄漏问题 4.使用场景 1)存储用户Session 2)解决线程安全的问题 3)使用ThreadLocal重新设计一个上下文设计模式 4)ThreadLocal注意事项 脏数据 内存泄漏 父子线程共享线程变量 1.什么是ThreadLocal变量 ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本. 这里有几点需要注意: 因为每个 T

  • Java ThreadLocal原理解析以及应用场景分析案例详解

    目录 ThreadLocal的定义 ThreadLocal的应用场景 ThreadLocal的demo TheadLocal的源码解析 ThreadLocal的set方法 ThreadLocal的get方法 ThreadLocalMap的结构 ThreadLocalMap的set方法 ThreadLocalMap的getEntry方法 ThreadLocal的内存泄露 如何避免内存泄露呢 应用实例 实际应用二 总结 ThreadLocal的定义 JDK对ThreadLocal的定义如下: The

  • Java ThreadLocal的详细解释

    目录 一.ThreadLocal简介 二.ThreadLocal简单使用 三.ThreadLocal的实现原理 1.set方法源码 2.get方法源码 3.remove方法的实现 4.如下图所示: 四.ThreadLocal不支持继承性 五.InheritableThreadLocal类 六.从ThreadLocalMap看ThreadLocal使用不当的内存泄漏问题 1.基础概念 2.分析ThreadLocalMap内部实现 总结: 一.ThreadLocal简介 多线程访问同一个共享变量的时

  • ThreadLocal原理介绍及应用场景

    本次给大家介绍重要的工具ThreadLocal.讲解内容如下,同时介绍什么场景下发生内存泄漏,如何复现内存泄漏,如何正确使用它来避免内存泄漏. ThreadLocal是什么?有哪些用途? ThreadLocal如何使用 ThreadLocal原理 ThreadLocal使用有哪些坑及注意事项 1. ThreadLocal是什么?有哪些用途? 首先介绍Thread类中属性threadLocals: /* ThreadLocal values pertaining to this thread. T

  • Docker 文件系统-AUFS 原理介绍

    目录 什么是联合文件系统 如何配置 Docker 的 AUFS 模式 AUFS 工作原理 AUFS 是如何存储文件的? AUFS 是如何工作的? 1. 读取文件 修改文件或目录 AUFS 演示 准备演示目录和文件 创建 AUFS 联合文件系统 验证 AUFS 的写时复制 结语 前言: 我们知道,Docker 主要是基于 Namespace.cgroups 和联合文件系统这三大核心技术实现的.前面的课时我详细讲解了 Namespace 和 cgroups 的相关原理,那么你知道联合文件系统是什么吗

  • Redis介绍和使用场景详解

    本篇文章主要介绍了Redis介绍和使用场景,小编觉得挺不错的,现在分享给大家,也给大家做个参考.一起跟随小编过来看看吧 (1)持久化数据库的缺点 平常我们使用的关系型数据库有Mysql.Oracle以及SqlServer等,在开发的过程中,数据通常都是通过Web提供的数据库驱动来链接数据库进行增删改查. 那么,我们日常使用的数据库的数据都储存在哪里呢?我们以Mysql为例. 打开我们Mysql所在的文件夹目录下的data文件夹,如下所示: 我们可以发现,里面都是我们创建的数据库,打开其中一个,可

  • Java多线程Condition接口原理介绍

    Condition接口提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的 Condition接口详解 Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁.Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,换句话说,Condition是依赖Lock对象的. Lock lock = new ReentrantL

  • 网络安全及防御之SQL注入原理介绍

    目录 A.SQL注入概述 什么是SQL注入 SQL注入的原理 SQL注入的地位 SQL注入的来源 SQL注入的主要特点 SQL注入的危害 B.SQL注入攻击 SQL注入攻击 SQL注入威胁表达方式 SQL注入典型攻击手段 SQL注入是否存在注入的判断方式 SQL注入实战步骤 按照注入点类型来分类 按照数据提交的方式来分类 按照执行效果来分类 C.MySQL注入常用函数 MySQL的注释风格 MySQL--union操作符 MySQL--常用函数 MySQL--字符串连接函数 MySQL--inf

  • 深入浅出解析Java ThreadLocal原理

    目录 1.了解ThreadLocal 简介 使用 2.源码解析 – 探究实现思路 threadLocals变量与ThreadLocalMap set(T value) 方法 get() 方法 remove() 方法 实现思路总结 3.InheritableThreadLocal与继承性 ThreadLocal的不可继承性 InheritableThreadLocal实现继承性的源码剖析 如何理解这个继承性 总结 4.存在的内存泄露问题 使用强引用会如何? 使用弱引用会如何? set().get(

  • Go语言中的Base64编码原理介绍以及使用

    目录 前言 Go Base64编码 什么是Base64编码 为什么需要Base64编码 Base64编码原理 编码步骤 位数不足情况 Base64解码原理 Base64标准编码变种 总结 前言 在网络中传递参数时,我们经常会对参数进行Base64编码,那么Go 语言中如何进行Base64编码呢?Base64编码的原理是怎样的呢?通过这篇文章一起来了解下吧! Go Base64编码 标准Base64编码 // 标准Base64编码     src := "hello world"   

  • Java中枚举的实现原理介绍

    目录 基本概述 使用方式 条件选择 循环遍历 集合映射 常用方法 总结 基本概述 在 JDK1.5 之前,通过定义常量使用的都是:public static fianl.而枚举的设计,就是把相关的常量分组到一个枚举类型里,方便创建和管理. 比如我们要定义一个颜色常量: public enum Colour { RED, YELLOW, BLUE, GREEN } 这段代码通过底层编译后,实际创建了4个枚举对象: new Enum<EnumTest>("RED", 0); n

随机推荐