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

相关文章

redis中使用lua脚本的原理与基本使用详解

《redis中使用lua脚本的原理与基本使用详解》在Redis中使用Lua脚本可以实现原子性操作、减少网络开销以及提高执行效率,下面小编就来和大家详细介绍一下在redis中使用lua脚本的原理... 目录Redis 执行 Lua 脚本的原理基本使用方法使用EVAL命令执行 Lua 脚本使用EVALSHA命令

Java Spring 中 @PostConstruct 注解使用原理及常见场景

《JavaSpring中@PostConstruct注解使用原理及常见场景》在JavaSpring中,@PostConstruct注解是一个非常实用的功能,它允许开发者在Spring容器完全初... 目录一、@PostConstruct 注解概述二、@PostConstruct 注解的基本使用2.1 基本代

Golang HashMap实现原理解析

《GolangHashMap实现原理解析》HashMap是一种基于哈希表实现的键值对存储结构,它通过哈希函数将键映射到数组的索引位置,支持高效的插入、查找和删除操作,:本文主要介绍GolangH... 目录HashMap是一种基于哈希表实现的键值对存储结构,它通过哈希函数将键映射到数组的索引位置,支持

C语言中位操作的实际应用举例

《C语言中位操作的实际应用举例》:本文主要介绍C语言中位操作的实际应用,总结了位操作的使用场景,并指出了需要注意的问题,如可读性、平台依赖性和溢出风险,文中通过代码介绍的非常详细,需要的朋友可以参... 目录1. 嵌入式系统与硬件寄存器操作2. 网络协议解析3. 图像处理与颜色编码4. 高效处理布尔标志集合

Java中的Lambda表达式及其应用小结

《Java中的Lambda表达式及其应用小结》Java中的Lambda表达式是一项极具创新性的特性,它使得Java代码更加简洁和高效,尤其是在集合操作和并行处理方面,:本文主要介绍Java中的La... 目录前言1. 什么是Lambda表达式?2. Lambda表达式的基本语法例子1:最简单的Lambda表

Python结合PyWebView库打造跨平台桌面应用

《Python结合PyWebView库打造跨平台桌面应用》随着Web技术的发展,将HTML/CSS/JavaScript与Python结合构建桌面应用成为可能,本文将系统讲解如何使用PyWebView... 目录一、技术原理与优势分析1.1 架构原理1.2 核心优势二、开发环境搭建2.1 安装依赖2.2 验

Java字符串操作技巧之语法、示例与应用场景分析

《Java字符串操作技巧之语法、示例与应用场景分析》在Java算法题和日常开发中,字符串处理是必备的核心技能,本文全面梳理Java中字符串的常用操作语法,结合代码示例、应用场景和避坑指南,可快速掌握字... 目录引言1. 基础操作1.1 创建字符串1.2 获取长度1.3 访问字符2. 字符串处理2.1 子字

Spring Boot循环依赖原理、解决方案与最佳实践(全解析)

《SpringBoot循环依赖原理、解决方案与最佳实践(全解析)》循环依赖指两个或多个Bean相互直接或间接引用,形成闭环依赖关系,:本文主要介绍SpringBoot循环依赖原理、解决方案与最... 目录一、循环依赖的本质与危害1.1 什么是循环依赖?1.2 核心危害二、Spring的三级缓存机制2.1 三

C#中async await异步关键字用法和异步的底层原理全解析

《C#中asyncawait异步关键字用法和异步的底层原理全解析》:本文主要介绍C#中asyncawait异步关键字用法和异步的底层原理全解析,本文给大家介绍的非常详细,对大家的学习或工作具有一... 目录C#异步编程一、异步编程基础二、异步方法的工作原理三、代码示例四、编译后的底层实现五、总结C#异步编程

SpringShell命令行之交互式Shell应用开发方式

《SpringShell命令行之交互式Shell应用开发方式》本文将深入探讨SpringShell的核心特性、实现方式及应用场景,帮助开发者掌握这一强大工具,具有很好的参考价值,希望对大家有所帮助,如... 目录引言一、Spring Shell概述二、创建命令类三、命令参数处理四、命令分组与帮助系统五、自定