⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 blog.csdn.net/zzg1229059735/ 「pony-zi」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

本次给大家介绍重要的工具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. 1.ThreadLocal是什么?有哪些用途?
  2. 2. 2.ThreadLocal如何使用
  3. 3. 3.ThreadLocal原理
    1. 3.0.1. ①ThreadLocal类结构及方法解析:
    2. 3.0.2. ②ThreadLocal及Thread之间的关系:
    3. 3.0.3. ③ThreadLocal的操作过程:
  • 4. 4.ThreadLocal使用有哪些坑及注意事项
    1. 4.0.1. ①所以我们测试下弱引用的回收机制:
    2. 4.0.2. ②ThreadLocal的弱引用回收情况