正确理解 ThreadLocal 的原理与应用场景

2024-06-21 06:38

本文主要是介绍正确理解 ThreadLocal 的原理与应用场景,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、ThreadLocal解决了什么问题

网上有很多关于ThreadLocal解决了什么问题的资料,但是很多都是不正确的。

不正确的理解

  • ThreadLocal为解决多线程程序的并发问题提供了一种新的思路
  • ThreadLocal的目的是为了解决多线程访问资源时的共享问题

正确的理解

看看JDK中的源码是怎么写的:

【原文】This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
【翻译】ThreadLocal类提了供线程本地变量。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的副本。ThreadLocal 变量通常被private static修饰,用于关联线程上下文。

ThreadLoal变量,它的基本原理是,同一个 ThreadLocal 所包含的对象(对ThreadLocal<String>而言即为 String 类型变量),在不同的 Thread 中有不同的副本。
因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。那就不存在多线程间共享资源的问题,既无共享,何来同步问题,又何来解决同步问题一说?

通过上面的分析,可以一句话总结ThreadLocal解决的问题:ThreadLocal提供了线程本地变量,每个线程都有一个该变量的副本,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

二、ThreadLocal应用场景

基于ThreadLocal解决的问题,我们可以将ThreadLocal应用到很多场景中。

  • 场景一:当一个变量需要在线程间隔离而在方法或类间共享时,可以使用ThreadLocal。
  • 场景二:……

三、ThreadLocal用法

public class ThreadLocalDemo {public static void main(String[] args) {new Thread(new InnerClass(),"Thread-1").start();new Thread(new InnerClass(),"Thread-2").start();new Thread(new InnerClass(),"Thread-3").start();}private static class InnerClass implements Runnable{@Overridepublic void run() {Counter.count.set(Counter.count.get()+1);System.out.println(Thread.currentThread().getName()+ ",count-hashcode="+Counter.count.hashCode()+",count-value="+Counter.count.get());}}private static class Counter{public static ThreadLocal<Integer> count = new ThreadLocal<Integer>(){@Overrideprotected Integer initialValue() {return 0;}};}
}//执行结果
Thread-2,count-hashcode=742865302,count-value=1
Thread-3,count-hashcode=742865302,count-value=1
Thread-1,count-hashcode=742865302,count-value=1

从上面的输出可以看出,每个线程的count对象是不一样。

四、ThreadLocal原理

方案一:ThreadLocal维护线程与实例之间的映射

  既然每个访问 ThreadLocal 变量的线程都有自己的一个“本地”实例副本。一个可能的方案就是 ThreadLocal 维护一个 Map,键是 Thread,值是它在该 Thread 内的实例。线程通过该 ThreadLocal 的 get() 方案获取实例时,只需要以线程为键,从 Map 中找出对应的实例即可。该方案如下图所示
这里写图片描述

  该方案可满足上文提到的每个线程内一个独立备份的要求。每个新线程访问该 ThreadLocal 时,需要向 Map 中添加一个映射,而每个线程结束时,应该清除该映射。这里就有两个问题:

  • 增加线程与减少线程均需要写 Map,故需保证该 Map线程安全。虽然从ConcurrentHashMap的演进看Java多线程核心技术一文介绍了几种实现线程安全 Map的方式,但它或多或少都需要锁来保证线程的安全性
  • 线程结束时,需要保证它所访问的所有 ThreadLocal 中对应的映射均删除,否则可能会引起内存泄漏(后文会介绍避免内存泄漏的方法)

其中锁的问题,是 JDK 未采用该方案的一个原因。

方案二:Thread维护ThreadLocal与实例的映射

  上述方案中,出现锁的问题,原因在于多线程访问同一个 Map。如果该 Map 由 Thread 维护,从而使得每个 Thread 只访问自己的 Map,那就不存在多线程写的问题,也就不需要锁。该方案如下图所示。
这里写图片描述
  该方案虽然没有锁的问题,但是由于每个线程访问某 ThreadLocal 变量后,都会在自己的 Map 内维护该 ThreadLocal 变量与具体实例的映射,如果不删除这些引用(映射),则这些 ThreadLocal 不能被回收,可能会造成内存泄漏。后文会介绍 JDK 如何解决该问题。

下面从源码的角度来看看具体实现。

set方法:

public void set(T value) {Thread t = Thread.currentThread();//获取当前线程维护的map。//Thread内部维护了一个ThreadLocal.ThreadLocalMap类型的变量,这个变量存储的就是线程本地变量ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);else//创建mapcreateMap(t, value);
}void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];//ThreadLocal变量在map的Entry数组中的位置,是由threadLocal对象的hashCode决定的int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);
}

get方法:

public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null)return (T)e.value;}//设置初始值return setInitialValue();
}

五、ThreadLocal如何防止内存泄漏

  ThreadLocalMap 的每个 Entry 都是一个对键(也就是ThreadLocal变量)的弱引用,这一点从super(k)可看出。另外,每个 Entry 都包含了一个对值的强引用。

static class Entry extends WeakReference<ThreadLocal> {Object value;Entry(ThreadLocal k, Object v) {//对键的弱引用super(k);//对值的强引用value = v;}
}

  因为对键使用的是弱引用,所以在没有其他强引用指向ThreadLocal变量时,它可被回收,从而避免上文所述ThreadLocal不能被回收而造成的内存泄漏的问题。
  但是,这里又可能出现另外一种内存泄漏的问题。ThreadLocalMap 维护 ThreadLocal 变量与具体实例的映射,当 ThreadLocal 变量被回收后,该映射的键变为 null,该 Entry 无法被移除。从而使得实例被该 Entry 引用而无法被回收造成内存泄漏。
  针对该问题,ThreadLocalMap 的 set 方法中,通过 replaceStaleEntry 方法将所有键为 null 的 Entry 的值设置为 null,从而使得该值可被回收。另外,会在 rehash 方法中通过 expungeStaleEntry 方法将键和值为 null 的 Entry 设置为 null 从而使得该 Entry 可被回收。通过这种方式,ThreadLocal 可防止内存泄漏。

这篇关于正确理解 ThreadLocal 的原理与应用场景的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1080472

相关文章

从原理到实战深入理解Java 断言assert

《从原理到实战深入理解Java断言assert》本文深入解析Java断言机制,涵盖语法、工作原理、启用方式及与异常的区别,推荐用于开发阶段的条件检查与状态验证,并强调生产环境应使用参数验证工具类替代... 目录深入理解 Java 断言(assert):从原理到实战引言:为什么需要断言?一、断言基础1.1 语

Python中re模块结合正则表达式的实际应用案例

《Python中re模块结合正则表达式的实际应用案例》Python中的re模块是用于处理正则表达式的强大工具,正则表达式是一种用来匹配字符串的模式,它可以在文本中搜索和匹配特定的字符串模式,这篇文章主... 目录前言re模块常用函数一、查看文本中是否包含 A 或 B 字符串二、替换多个关键词为统一格式三、提

Java MQTT实战应用

《JavaMQTT实战应用》本文详解MQTT协议,涵盖其发布/订阅机制、低功耗高效特性、三种服务质量等级(QoS0/1/2),以及客户端、代理、主题的核心概念,最后提供Linux部署教程、Sprin... 目录一、MQTT协议二、MQTT优点三、三种服务质量等级四、客户端、代理、主题1. 客户端(Clien

ModelMapper基本使用和常见场景示例详解

《ModelMapper基本使用和常见场景示例详解》ModelMapper是Java对象映射库,支持自动映射、自定义规则、集合转换及高级配置(如匹配策略、转换器),可集成SpringBoot,减少样板... 目录1. 添加依赖2. 基本用法示例:简单对象映射3. 自定义映射规则4. 集合映射5. 高级配置匹

MySQL中的表连接原理分析

《MySQL中的表连接原理分析》:本文主要介绍MySQL中的表连接原理分析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、背景2、环境3、表连接原理【1】驱动表和被驱动表【2】内连接【3】外连接【4编程】嵌套循环连接【5】join buffer4、总结1、背景

深度解析Spring AOP @Aspect 原理、实战与最佳实践教程

《深度解析SpringAOP@Aspect原理、实战与最佳实践教程》文章系统讲解了SpringAOP核心概念、实现方式及原理,涵盖横切关注点分离、代理机制(JDK/CGLIB)、切入点类型、性能... 目录1. @ASPect 核心概念1.1 AOP 编程范式1.2 @Aspect 关键特性2. 完整代码实

python中Hash使用场景分析

《python中Hash使用场景分析》Python的hash()函数用于获取对象哈希值,常用于字典和集合,不可变类型可哈希,可变类型不可,常见算法包括除法、乘法、平方取中和随机数哈希,各有优缺点,需根... 目录python中的 Hash除法哈希算法乘法哈希算法平方取中法随机数哈希算法小结在Python中,

Java Stream的distinct去重原理分析

《JavaStream的distinct去重原理分析》Javastream中的distinct方法用于去除流中的重复元素,它返回一个包含过滤后唯一元素的新流,该方法会根据元素的hashcode和eq... 目录一、distinct 的基础用法与核心特性二、distinct 的底层实现原理1. 顺序流中的去重

Spring @Scheduled注解及工作原理

《Spring@Scheduled注解及工作原理》Spring的@Scheduled注解用于标记定时任务,无需额外库,需配置@EnableScheduling,设置fixedRate、fixedDe... 目录1.@Scheduled注解定义2.配置 @Scheduled2.1 开启定时任务支持2.2 创建

Spring Boot 实现 IP 限流的原理、实践与利弊解析

《SpringBoot实现IP限流的原理、实践与利弊解析》在SpringBoot中实现IP限流是一种简单而有效的方式来保障系统的稳定性和可用性,本文给大家介绍SpringBoot实现IP限... 目录一、引言二、IP 限流原理2.1 令牌桶算法2.2 漏桶算法三、使用场景3.1 防止恶意攻击3.2 控制资源