正确理解 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

相关文章

防止Linux rm命令误操作的多场景防护方案与实践

《防止Linuxrm命令误操作的多场景防护方案与实践》在Linux系统中,rm命令是删除文件和目录的高效工具,但一旦误操作,如执行rm-rf/或rm-rf/*,极易导致系统数据灾难,本文针对不同场景... 目录引言理解 rm 命令及误操作风险rm 命令基础常见误操作案例防护方案使用 rm编程 别名及安全删除

PHP应用中处理限流和API节流的最佳实践

《PHP应用中处理限流和API节流的最佳实践》限流和API节流对于确保Web应用程序的可靠性、安全性和可扩展性至关重要,本文将详细介绍PHP应用中处理限流和API节流的最佳实践,下面就来和小编一起学习... 目录限流的重要性在 php 中实施限流的最佳实践使用集中式存储进行状态管理(如 Redis)采用滑动

ShardingProxy读写分离之原理、配置与实践过程

《ShardingProxy读写分离之原理、配置与实践过程》ShardingProxy是ApacheShardingSphere的数据库中间件,通过三层架构实现读写分离,解决高并发场景下数据库性能瓶... 目录一、ShardingProxy技术定位与读写分离核心价值1.1 技术定位1.2 读写分离核心价值二

深度解析Python中递归下降解析器的原理与实现

《深度解析Python中递归下降解析器的原理与实现》在编译器设计、配置文件处理和数据转换领域,递归下降解析器是最常用且最直观的解析技术,本文将详细介绍递归下降解析器的原理与实现,感兴趣的小伙伴可以跟随... 目录引言:解析器的核心价值一、递归下降解析器基础1.1 核心概念解析1.2 基本架构二、简单算术表达

深入浅出Spring中的@Autowired自动注入的工作原理及实践应用

《深入浅出Spring中的@Autowired自动注入的工作原理及实践应用》在Spring框架的学习旅程中,@Autowired无疑是一个高频出现却又让初学者头疼的注解,它看似简单,却蕴含着Sprin... 目录深入浅出Spring中的@Autowired:自动注入的奥秘什么是依赖注入?@Autowired

从原理到实战解析Java Stream 的并行流性能优化

《从原理到实战解析JavaStream的并行流性能优化》本文给大家介绍JavaStream的并行流性能优化:从原理到实战的全攻略,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的... 目录一、并行流的核心原理与适用场景二、性能优化的核心策略1. 合理设置并行度:打破默认阈值2. 避免装箱

PostgreSQL简介及实战应用

《PostgreSQL简介及实战应用》PostgreSQL是一种功能强大的开源关系型数据库管理系统,以其稳定性、高性能、扩展性和复杂查询能力在众多项目中得到广泛应用,本文将从基础概念讲起,逐步深入到高... 目录前言1. PostgreSQL基础1.1 PostgreSQL简介1.2 基础语法1.3 数据库

Python中的filter() 函数的工作原理及应用技巧

《Python中的filter()函数的工作原理及应用技巧》Python的filter()函数用于筛选序列元素,返回迭代器,适合函数式编程,相比列表推导式,内存更优,尤其适用于大数据集,结合lamb... 目录前言一、基本概念基本语法二、使用方式1. 使用 lambda 函数2. 使用普通函数3. 使用 N

Python中yield的用法和实际应用示例

《Python中yield的用法和实际应用示例》在Python中,yield关键字主要用于生成器函数(generatorfunctions)中,其目的是使函数能够像迭代器一样工作,即可以被遍历,但不会... 目录python中yield的用法详解一、引言二、yield的基本用法1、yield与生成器2、yi

Spring Security 前后端分离场景下的会话并发管理

《SpringSecurity前后端分离场景下的会话并发管理》本文介绍了在前后端分离架构下实现SpringSecurity会话并发管理的问题,传统Web开发中只需简单配置sessionManage... 目录背景分析传统 web 开发中的 sessionManagement 入口ConcurrentSess