ThreadLocal(全)- 代码实现

简介: ThreadLocal是Java中一个非常重要的线程技术。它可以让每个线程都拥有自己的变量副本,避免了线程间的竞争和数据泄露问题。在本文中,我们将详细介绍ThreadLocal的定义、用法及其优点。ThreadLocal是Java中一个用来实现线程封闭技术的类。它提供了一个本地线程变量,可以在多线程环境下使每个线程都拥有自己的变量副本。每个线程都可以独立地改变自己的副本,而不会影响到其他线程的副本。ThreadLocal的实现是基于ThreadLocalMap的,每个ThreadLocal对象都对应一个ThreadLocalMap,其中存储了线程本地变量的值。

一、优缺点

ThreadLocal的主要优点是可以提高并发程序的性能和安全性,同时也存在一些缺点和使用场景需要注意。


优点:

1.提高并发性能:使用ThreadLocal可以避免多个线程之间的竞争,从而提高程序的并发性能。

2.保证线程安全:每个线程有自己独立的变量副本,避免了线程安全问题。

3.简化代码:使用ThreadLocal可以避免传递参数的繁琐,简化代码。

缺点:

1.内存泄漏:ThreadLocal变量副本的生命周期与线程的生命周期一样长,如果线程长时间存在,而ThreadLocal变量没有及时清理,就会造成内存泄漏。

2.增加资源开销:每个线程都要创建一个独立的变量副本,如果线程数很多,就会增加资源开销。

3.不适用于共享变量:ThreadLocal适用于每个线程有独立的变量副本的场景,不适用于共享变量的场景。

二、适用场景

1.线程安全的对象:ThreadLocal适用于需要在多个线程中使用的线程安全对象,例如SimpleDateFormat、Random等。

2.跨层传递参数:ThreadLocal可以避免在方法之间传递参数的繁琐,尤其在跨层传递参数的场景中,可以大大简化代码。

3.线程局部变量:ThreadLocal可以用于在当前线程中存储和访问局部变量,例如日志、请求信息等。

三、实现原理

首先通过一张图看下ThreadLocal与线程的关系图:

1.每个Thread对象都有一个ThreadLocalMap类型的成员变量threadLocals,这个变量是一个键值对集合,用于存储每个ThreadLocal对象对应的值。

2.每个ThreadLocal对象都有一个唯一的ID,用于在ThreadLocalMap中作为键来存储值。

3.当一个线程第一次调用ThreadLocal对象的get()方法时,它会先获取当前线程的ThreadLocalMap对象,然后以ThreadLocal对象的ID作为键,从ThreadLocalMap中获取对应的值。

4.如果ThreadLocalMap中不存在对应的键值对,则调用ThreadLocal对象的initialValue()方法来初始化一个值,并将其存储到ThreadLocalMap中。

5.如果ThreadLocalMap对象的引用不再需要,那么需要手动将其置为null,这样可以避免内存泄漏。

内存泄漏

ThreadLocal变量副本的生命周期与线程的生命周期一样长,如果线程长时间存在,而ThreadLocal变量没有及时清理,就会造成内存泄漏。为了避免内存泄漏,可以在使用ThreadLocal的地方及时清理ThreadLocal变量,例如在线程池中使用ThreadLocal时,需要在线程结束时手动清理ThreadLocal变量。


内存泄漏出现的原因:

ThreadLocalMap中的Entry对象持有ThreadLocal对象的弱引用,但是ThreadLocalMap中的Entry对象是由ThreadLocal对象强引用的。
如果ThreadLocal对象没有及时清理,在ThreadLocal对象被垃圾回收时,ThreadLocalMap中的Entry对象仍然存在,从而导致内存泄漏。

解决内存泄漏的方法:

在使用ThreadLocal的代码中及时清理ThreadLocal变量。通常情况下,我们可以使用ThreadLocal的remove()方法手动清理ThreadLocal
变量,或者在使用完ThreadLocal变量后将其设置为null。

通过上图我们可以看到,在线程方法执行过程中,ThreadLocal、ThreadLocalMap以及Thread之间的引用关系; Thread中存在一个属性threadLocals指向了ThreadLocalMap,ThreadLocal实现线程级别的数据隔离主要是基于该对象;在ThreadLocal中是没有存储任何数据,其更像一个线程与ThreadLocalMap间的协调器,数据存储在ThreadLocalMap中,但是该Map的Key却是ThreadLocal的弱引用;


一般情况下,线程执行完成后,待线程销毁,那么线程对应的属性threadLocals也会被销毁;但是真实环境中对线程的使用大部分都是线程池,这样在整个系统生命周期中, 线程都是有效的,直至线程池关闭。而将ThreadLocalMap的Key设置成弱引用时,经过GC后该Map的Key则变成了null,但是其Value却一直存在,因此需要手动将key为null 的数据进行清理。


下面是一个示例演示如何避免ThreadLocal内存泄漏:

public class MyThreadLocal {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    public static void set(String value) {
        threadLocal.set(value);
    }
    public static String get() {
        return threadLocal.get();
    }
    public static void remove() {
        threadLocal.remove();
    }
}
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        MyThreadLocal.set("hello");
        System.out.println(MyThreadLocal.get());
        // 在使用完ThreadLocal变量后,调用remove()方法清理ThreadLocal变量
        MyThreadLocal.remove();
    }
}

在上面的代码中,MyThreadLocal类封装了ThreadLocal变量的操作,MyRunnable类实现了Runnable接口,使用MyThreadLocal类来存储和访问 ThreadLocal变量。在MyRunnable的run()方法中,使用完ThreadLocal变量后,调用remove()方法清理ThreadLocal变量,避免了内存泄漏的问题。


ThreadLocal一般会设置成static


主要是为了避免重复创建TSO(thread specific object,即与线程相关的变量。)我们知道,一个ThreadLocal实例对应当前线程中的一个TSO实例。如果把ThreadLocal声明为某个类的实例变量(而不是静态变量),那么每创建一个该类的实例就会导致一个新的TSO实例被创建。而这些被创建的TSO实例是同一个类的实例。同一个线程可能会访问到同一个TSO(指类)的不同实例,这即便不会导致错误,也会导致浪费!


简单的说就是在ThreadLocalMap中,同一个线程是否有必要设置多个ThreadLocal来存储线程变量?


示例

下面是一个简单的例子,演示了如何使用ThreadLocal来实现线程数据隔离:

在上面的代码中,MyThreadLocal类封装了ThreadLocal变量的操作,MyRunnable类实现了Runnable接口,使用MyThreadLocal类来存储和访问 ThreadLocal变量。在MyRunnable的run()方法中,使用完ThreadLocal变量后,调用remove()方法清理ThreadLocal变量,避免了内存泄漏的问题。
ThreadLocal一般会设置成static
主要是为了避免重复创建TSO(thread specific object,即与线程相关的变量。)我们知道,一个ThreadLocal实例对应当前线程中的一个TSO实例。如果把ThreadLocal声明为某个类的实例变量(而不是静态变量),那么每创建一个该类的实例就会导致一个新的TSO实例被创建。而这些被创建的TSO实例是同一个类的实例。同一个线程可能会访问到同一个TSO(指类)的不同实例,这即便不会导致错误,也会导致浪费!
简单的说就是在ThreadLocalMap中,同一个线程是否有必要设置多个ThreadLocal来存储线程变量?
示例
下面是一个简单的例子,演示了如何使用ThreadLocal来实现线程数据隔离:

运行结果如下:

Thread A: Thread A
Thread B: Thread B
Main: null

从输出结果可以看出,每个线程都拥有自己的变量副本,互不影响。而在主线程中,由于没有设置过变量副本,所以返回null。

结束语

ThreadLocal是帮助我们在多个线程间实现线程对数据独享,并不是用来解决线程间的数据共享问题。

相关文章
|
分布式计算 大数据 数据处理
Apache Spark:提升大规模数据处理效率的秘籍
【4月更文挑战第7天】本文介绍了Apache Spark的大数据处理优势和核心特性,包括内存计算、RDD、一站式解决方案。分享了Spark实战技巧,如选择部署模式、优化作业执行流程、管理内存与磁盘、Spark SQL优化及监控调优工具的使用。通过这些秘籍,可以提升大规模数据处理效率,发挥Spark在实际项目中的潜力。
1054 0
|
存储 缓存 安全
【cmake 生成配置文件】CMake与现代C++:配置文件宏的深度探索与应用
【cmake 生成配置文件】CMake与现代C++:配置文件宏的深度探索与应用
516 0
小技巧 - 微信零钱转出免手续费方法(利用零钱通转出)
小技巧 - 微信零钱转出免手续费方法(利用零钱通转出)
4206 0
小技巧 - 微信零钱转出免手续费方法(利用零钱通转出)
|
人工智能 自然语言处理 数据挖掘
产品更新|宜搭AI 新增「智能数据分析」「智能表单」两项功能!
「宜搭AI」开放新一期功能:智能数据分析、智能表单,已支持在宜搭网页端使用体验。
902 0
产品更新|宜搭AI 新增「智能数据分析」「智能表单」两项功能!
|
缓存 Linux 开发者
Linux内核中的并发控制机制:深入理解与应用####
【10月更文挑战第21天】 本文旨在为读者提供一个全面的指南,探讨Linux操作系统中用于实现多线程和进程间同步的关键技术——并发控制机制。通过剖析互斥锁、自旋锁、读写锁等核心概念及其在实际场景中的应用,本文将帮助开发者更好地理解和运用这些工具来构建高效且稳定的应用程序。 ####
254 5
|
Java
Java Character 类详解
`Character` 类是 Java 中的一个封装类,位于 `java.lang` 包中,主要用于处理单个字符。它是一个最终类,提供了多种静态方法来检查和操作字符属性,如判断字符是否为字母、数字或空格,以及转换字符的大小写等。此外,`Character` 类还支持自动装箱和拆箱,简化了 `char` 和 `Character` 之间的转换。以下是一些示例代码,展示了如何使用 `Character` 类的方法来检查字符属性和执行字符转换。掌握 `Character` 类的用法有助于更高效地处理字符数据。
725 2
|
机器学习/深度学习 算法 数据挖掘
【数据挖掘】 GBDT面试题:其中基分类器CART回归树,节点的分裂标准是什么?与RF的区别?与XGB的区别?
文章讨论了梯度提升决策树(GBDT)中的基分类器CART回归树的节点分裂标准,并比较了GBDT与随机森林(RF)和XGBoost(XGB)的区别,包括集成学习方式、偏差-方差权衡、样本使用、并行性、最终结果融合、数据敏感性以及泛化能力等方面的不同。
357 1
|
Shell
shell搜索文件和内容
shell搜索文件和内容
483 1
|
算法 NoSQL 关系型数据库
九种分布式ID解决方案
在复杂的分布式系统中,往往需要对大量的数据进行唯一标识,比如在对一个订单表进行了分库分表操作,这时候数据库的自增ID显然不能作为某个订单的唯一标识。除此之外还有其他分布式场景对分布式ID的一些要求:
1382 0
|
监控 大数据 数据处理
大数据组件之Storm简介
【5月更文挑战第2天】Apache Storm是用于实时大数据处理的分布式系统,提供容错和高可用的实时计算。核心概念包括Topology(由Spouts和Bolts构成的DAG)、Spouts(数据源)和Bolts(数据处理器)。Storm通过acker机制确保数据完整性。常见问题包括数据丢失、性能瓶颈和容错理解不足。避免这些问题的方法包括深入学习架构、监控日志、性能调优和编写健壮逻辑。示例展示了实现单词计数的简单Topology。进阶话题涵盖数据延迟、倾斜的处理,以及Trident状态管理和高级实践,强调调试、性能优化和数据安全性。
794 4