ThreadLocal深度解析

简介

在并发编程中,导致并发bug的问题都会归结于对共享变量的操作不当。多个线程同时读写同一共享变量存在并发问题,我们可以利用写时复制、不变性来突破对原数据的写操作,没有写就没有并发问题,而本篇文章所介绍的技术是突破共享变量,没有共享变量也不会有并发问题。

Java中避免线程共享的一大利器就是ThreadLocal,我们本篇文章重点讲述它的底层原理、常见的一些用途、创建和使用等。

首先介绍一下它是什么:

  • ThreadLocal 是 Java 的一个类,位于 java.lang 包下。它为每一个线程提供了一个独立的变量副本,使得每个线程可以独立地改变自己的副本,而不会影响其他线程所对应的副本。

它的主要作用就是避免线程间共享,除此之外还能避免无必要的同步以及降低变量使用的复杂度:

  • 线程隔离:在多线程环境下,ThreadLocal 可以为每一个线程提供一个独立的变量实例。因此,每个线程都可以独立地操作该变量,而不需要考虑并发问题。这种特性使得 ThreadLocal 成为一个非常方便的保存线程状态或线程局部变量的工具。
  • 避免无必要的同步:由于每个线程都有其自己的变量副本,所以它们可以无需任何同步措施就可以访问这些变量,从而提高了程序的执行效率。
  • 降低复杂度:在某些场景下,如果需要通过参数传递线程局部变量,可能会使得方法签名和调用变得复杂。使用 ThreadLocal 可以避免这种复杂性,因为它允许我们在任何地方获取到线程相关的数据。

工作原理

我们在学习ThreadLocal的时候,如果不了解其底层实现的话,通常会进入一个误区:ThreadLocal类里面维护了一个Map,这个Map的key为线程引用,值为自己设置的值。其实Java真正的实现是ThreadLocal仅是一个工具类,提供线程对数据的访问,实际真正拥有这个数据的角色是线程自身(这也解释了简介中提到的每个线程都有针对某一变量的副本),如下图所示:

在这里插入图片描述

相信大家会有这样一个疑问:为什么Java没有按照我们预想的那样实现呢?其主要原因就是:不容易造成内存泄漏。假设Java按照我们预想的方案实现,我们分析下会造成什么样的问题:ThreadLocal 持有的 Map 会持有 Thread 对象的引用,这就意味着,只要 ThreadLocal 对象存在,那么 Map 中的 Thread 对象就永远不会被回收。ThreadLocal 的生命周期往往都比线程要长,所以这种设计方案很容易导致内存泄露。而 Java 的实现中 Thread 持有 ThreadLocalMap,而且 ThreadLocalMap 里对 ThreadLocal 的引用还是弱引用(WeakReference),所以只要 Thread 对象可以被回收,那么 ThreadLocalMap 就能被回收。Java 的这种实现方案虽然看上去复杂一些,但是更加安全。

另外这里面的关于Entry[]大小的问题有个讨论点:为什么ThreadLocalMap中的Entry[]大小一定是2的幂次方呢?

ThreadLocalMap大小

其主要原因如下:

  • 优化索引计算:当 table 大小为 2 的次幂时,计算索引位置可以通过简单的位操作实现,而不需要执行昂贵的除法操作。例如,如果 table 大小为 16,那么为了得到一个 key 对应的索引位置,只需要计算 key的hashCode & (table.length - 1)。这种位操作比标准的模运算(%)速度快得多。
  • 均匀分布:2 的次幂大小确保了哈希值在 table 中均匀分布,减少了哈希冲突的可能性。这是因为当我们使用 hashCode & (table.length - 1) 这种方式时,hash值的低位将被有效地使用来计算索引,这在大多数情况下可以确保数据在 table 中均匀分布。
  • 容易扩容:当 table 需要扩容时(例如,当存储的元素太多,达到了负载因子的限制),新的大小可以简单地翻倍,仍然保持为 2 的次幂。
  • 历史与一致性:Java 中的其他哈希结构,如 HashMap,也使用了类似的策略,因此保持这种做法在某种程度上也是为了一致性。

总的来说,选择 2 的次幂作为 table 大小是一个优化决策,旨在提高性能、减少哈希冲突并简化内部操作。

常见用途

  1. 数据库连接:使用 ThreadLocal 保存每个线程独立的数据库连接,确保在同一线程中共享同一个数据库连接,而不是频繁地创建和销毁。
  2. 日期格式化:SimpleDateFormat 不是线程安全的,但我们可以用 ThreadLocal 来为每个线程创建独立的 SimpleDateFormat 实例,避免并发问题。
  3. Web 会话管理:在 Web 应用中,可以使用 ThreadLocal 来保存当前请求的用户会话或其他会话相关的信息。
  4. 框架中的上下文信息:许多框架使用 ThreadLocal 来保存线程级别的上下文数据,例如 Spring 中的事务管理。

创建和使用

下面使用ThreadLocal演示一下为每一个线程分配一个线程ID的代码,不同线程拥有不同的线程ID。

package com.markus.concurrent;

import java.util.concurrent.atomic.AtomicLong;

/**
 * @author: markus
 * @date: 2023/8/20 4:16 PM
 * @Description:
 * @Blog: https://markuszhang.com
 * It's my honor to share what I've learned with you!
 */
public class ThreadLocalDemo {

    static class ThreadID {
        static final AtomicLong nextId = new AtomicLong(0);
        static final ThreadLocal<Long> tl = ThreadLocal.withInitial(nextId::incrementAndGet);

        public static Long get() {
            return tl.get();
        }
    }

    static class GetThreadId implements Runnable {

        @Override
        public void run() {
            for (int i = 0; i < 3; i++) {
                System.out.println("Thread [" + Thread.currentThread().getName() + "],its id is " + ThreadID.get());
            }
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(new GetThreadId(), "t1");
        Thread t2 = new Thread(new GetThreadId(), "t2");
        t1.start();
        t2.start();
    }
}

控制台打印:

控制台

优势、缺点与陷阱

在多线程编程中,ThreadLocal 是一个不可或缺的工具。其主要优势在于为每个线程提供了私有的、独立的变量副本,从而消除了多线程并发访问的问题。这种独立性确保了每个线程都可以无需同步就能访问其局部变量,大大提高了程序的并发性和效率。此外,它还能简化代码结构,避免了复杂的参数传递,为线程关联上下文信息提供了便捷手段,如日志记录中的会话ID。

但是,与其带来的好处相伴的是一些潜在的陷阱。最为严重的问题是与内存泄漏相关。因为ThreadLocal 使用弱引用来引用键,但其对应的值则是强引用。如果不适时地清理,尤其是在线程生命周期结束前不调用 remove() 方法,可能会造成内存泄漏。此外,过度或不适当地使用 ThreadLocal 可能导致代码结构混乱,并降低代码的可读性和维护性。

总之,当我们在项目中使用 ThreadLocal 时,应该始终注意其潜在的陷阱和限制。正确、合理地使用 ThreadLocal 可以为我们带来很多好处,但不恰当的使用可能会引发一系列问题。在使用前,充分了解其工作机制并持续关注是非常必要的。

InheritableThreadLocal

顾名思义,可继承的ThreadLocal。我们知道ThreadLocalMap是线程持有的,每个变量都在各自的线程中保存一个副本,每个线程都能拿到自己的值,但是如果父线程创建了一个子线程,那么这个子线程是无法拿到父线程中的ThreadLocalMap中的信息的。InheritableThreadLocal的出现就是解决这个问题的。它继承自ThreadLocal,相关用法与ThreadLocal一致,这里就不介绍了。

最佳实践

  • 限制使用范围:仅在确实需要线程局部变量来解决问题时使用 ThreadLocal。不应该过度使用或在不必要的场合使用它。

  • 及时清理:

    • 一定要在不再需要线程局部变量时调用 ThreadLocalremove() 方法,以释放资源并避免潜在的内存泄漏。
    • 在 Web 容器中,例如 Tomcat,确保在请求结束后清除 ThreadLocal,因为容器可能会重用线程。
  • 避免长时间存活的线程局部变量:如果一个线程预计会长时间存活而不释放(例如线程池中的线程),那么这种线程中的 ThreadLocal 变量也会长时间存活。在这种情况下,应该特别注意清理这些变量。

  • 不要存储大对象:为了避免内存消耗,不应该在 ThreadLocal 中存储大对象或那些你不打算很快释放的对象。、

  • 考虑使用 InheritableThreadLocal:当需要在一个线程及其所有子线程之间共享一个变量时,可以考虑使用 InheritableThreadLocal。但要注意,如果子线程修改了这个变量,它不会影响到父线程中的值。

  • 考虑线程安全性:虽然 ThreadLocal 变量不需要同步来保证线程安全,但存储在其中的对象仍然可能需要同步,尤其是当多个线程可能访问同一个 ThreadLocal 变量中的同一个实例时(例如,使用 InheritableThreadLocal)。

  • 优先使用库提供的线程局部功能:有些库或框架可能已经提供了其自己的线程局部功能,例如 Java EE 和 Spring。在这种情况下,优先使用库或框架提供的功能,除非有特定的需求使得必须使用原生的 ThreadLocal

  • 不要将 ThreadLocal 作为全局变量:尽量避免将 ThreadLocal 作为静态变量,除非你确实需要这样做。静态的 ThreadLocal 变量可能会导致预料之外的问题,尤其是当类被卸载时。

  • 不建议你在线程池中使用 InheritableThreadLocal,不仅仅是因为它具有 ThreadLocal 相同的缺点——可能导致内存泄露,更重要的原因是:线程池中线程的创建是动态的,很容易导致继承关系错乱,如果你的业务逻辑依赖 InheritableThreadLocal,那么很可能导致业务逻辑计算错误,而这个错误往往比内存泄露更要命

总之,ThreadLocal 是一个有用但需要小心使用的工具。确保了解其工作原理,并始终遵循上述的最佳实践,这样可以最大化其价值并避免潜在的问题。

总结

好了,简单总结一下我们本篇讲述的内容,首先讲述了ThreadLocal是什么以及它的作用是什么,并通过图片展示ThreadLocal模型,知道了ThreadLocal其实就是一个工具类,内部不保存数据,真正保存数据的地方是在Thread本身,也就是我们常说的每个Thread都有一个变量的副本。也介绍了ThreadLocalMap中的Entry[]数组为什么大小一定要限制为2的幂次方,后面通过一段代码简单演示了ThreadLocal的使用,并再次声明了它的优势和其缺点以及使用不到出现的陷阱。讲述过程中提到了父子线程不共享变量副本的问题,而Java提供了InheritableThreadLocal解决这一问题。最终我们罗列了ThreadLocal的最佳实践。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/84800.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

初始C语言(7)——详细讲解有关初阶指针的内容

系列文章目录 第一章 “C“浒传——初识C语言&#xff08;1&#xff09;&#xff08;更适合初学者体质哦&#xff01;&#xff09; 第二章 初始C语言&#xff08;2&#xff09;——详细认识分支语句和循环语句以及他们的易错点 第三章 初阶C语言&#xff08;3&#xff09;——…

arm:day6

实现UART通信&#xff1a; 1.键盘输入一个字符a,串口工具显示b 2.键盘输入一个字符串"nihao",串口工具显示"nihao" uart.h #ifndef __UART4_H__ #define __UART4_H__#include "stm32mp1xx_uart.h" #include "stm32mp1xx_gpio.h" #in…

用于智能图像处理的计算机视觉和 NLP

莫斯科&#xff0c;神秘之城...&#xff08;这张照片由伊戈尔沙巴林提供&#xff09; 一、说明 如今&#xff0c;每个拥有智能手机的人都可能成为摄影师。因此&#xff0c;每天都有大量新照片出现在社交媒体、网站、博客和个人照片库中。尽管拍照的过程可能非常令人兴奋&#x…

Unity解决:3D开发模式第三人称视角 WASD控制角色移动旋转 使用InputSystem

Unity版本&#xff1a;2019.2.3f1 目录 安装InputSystem 1&#xff1a;创建InputHander.cs脚本 挂载到Player物体上 获取键盘输入WADS 2.创建PlayerLocomotion.cs挂载到Player物体上&#xff0c;控制物体移动转向 安装InputSystem 菜单栏/Window/Package Manager/Input Syst…

CentOS中Oracle11g进程有哪些

最近遇到Oracle数据库运行过程实例进程由于某种原因导致中止的问题&#xff0c;专门看了下正常Oracle数据库启动后的进程有哪些&#xff0c;查阅资料了解了下各进程的作用&#xff0c;记录如下。 oracle 3032 1 0 07:36 ? 00:00:00 ora_pmon_orcl oracle …

Linux:安全技术与防火墙

目录 一、安全技术 1.安全技术 2.防火墙的分类 3.防水墙 4.netfilter/iptables关系 二、防火墙 1、iptables四表五链 2、黑白名单 3.iptables命令 3.1查看filter表所有链 iptables -L ​编辑3.2用数字形式(fliter)表所有链 查看输出结果 iptables -nL 3.3 清空所有链…

计算机竞赛 垃圾邮件(短信)分类算法实现 机器学习 深度学习

文章目录 0 前言2 垃圾短信/邮件 分类算法 原理2.1 常用的分类器 - 贝叶斯分类器 3 数据集介绍4 数据预处理5 特征提取6 训练分类器7 综合测试结果8 其他模型方法9 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 垃圾邮件(短信)分类算…

UE4/UE5 “无法双击打开.uproject 点击无反应“解决

一、方法一&#xff1a;运行UnrealVersionSelector.exe 1.找到Epic Game Lancher的安装目录&#xff0c; 在lancher->Engine->Binaries->Win64->UnrealVersionSelector.exe 2.把UnrealVersionSelector.exe 分别拷贝到UE4 不同版本引擎的 Engine->Binaries->…

「UG/NX」Block UI 体收集器BodyCollector

✨博客主页何曾参静谧的博客📌文章专栏「UG/NX」BlockUI集合📚全部专栏「UG/NX」NX二次开发「UG/NX」BlockUI集合「VS」Visual Studio「QT」QT5程序设计「C/C+&#

创建型(二) - 单例模式

一、概念 单例设计模式&#xff08;Singleton Design Pattern&#xff09;&#xff1a;一个类只允许创建一个对象&#xff08;或者实例&#xff09;&#xff0c;那这个类就是一个单例类。 优点&#xff1a;在内存里只有一个实例&#xff0c;减少了内存的开销&#xff0c;避免…

.NET应用UI组件DevExpress XAF v23.1 - 全新的日程模块

DevExpress XAF是一款强大的现代应用程序框架&#xff0c;允许同时开发ASP.NET和WinForms。DevExpress XAF采用模块化设计&#xff0c;开发人员可以选择内建模块&#xff0c;也可以自行创建&#xff0c;从而以更快的速度和比开发人员当前更强有力的方式创建应用程序。 在新版中…

chatGPT-对话柏拉图

引言&#xff1a; 古希腊哲学家柏拉图&#xff0c;在他的众多著作中&#xff0c;尤以《理想国》为人所熟知。在这部杰作中&#xff0c;他勾勒了一个理想的政治制度&#xff0c;提出了各种政体&#xff0c;并阐述了他对于公正、智慧以及政治稳定的哲学观点。然而&#xff0c;其…

使用Jetpack Compose的镜像效果

使用Jetpack Compose的镜像效果 您是否曾想过在列表或一般情况下为图像创建镜像效果&#xff1f;有了强大的Jetpack Compose UI工具包&#xff0c;这变得简单而容易。 正如您所看到的&#xff0c;此效果包括以下内容 反转图像反转图像的50&#xff05;可见性模糊的反转图像与…

5、css学习5(链接、列表)

1、css可以设置链接的四种状态样式。 a:link - 正常&#xff0c;未访问过的链接a:visited - 用户已访问过的链接a:hover - 当用户鼠标放在链接上时a:active - 链接被点击的那一刻 2、 a:hover 必须在 a:link 和 a:visited 之后&#xff0c; a:active 必须在 a:hover 之后&…

ElasticSearch7.x + kibana7.x使用记录

目录 查询所有索引 查询索引的mapping信息 添加索引的同时添加mapping 在原有基础上新增字段 旧的索引迁移到新的索引&#xff08;使用场景&#xff1a;数据迁移、索引优化、数据转换&#xff09; 查询索引下的文档总数 场景1&#xff1a;某一个字段的值是数组&#xff0…

回归预测 | MATLAB实现WOA-SVM鲸鱼算法优化支持向量机多输入单输出回归预测(多指标,多图)

回归预测 | MATLAB实现WOA-SVM鲸鱼算法优化支持向量机多输入单输出回归预测&#xff08;多指标&#xff0c;多图&#xff09; 目录 回归预测 | MATLAB实现WOA-SVM鲸鱼算法优化支持向量机多输入单输出回归预测&#xff08;多指标&#xff0c;多图&#xff09;效果一览基本介绍程…

【操作系统】寄存器

概念 寄存器是CPU内部用来存放数据的一些小型存储区域&#xff0c;用来暂时存放参与运算的数据和运算结果。其实寄存器就是一种常用的时序逻辑电路&#xff0c;但这种时序逻辑电路只包含存储电路。寄存器的存储电路是由锁存器或触发器构成的&#xff0c;因为一个锁存器或触发器…

作为一名8年测试工程师,因为偷偷接私活被····

接私活 对程序员这个圈子来说是一个既公开又隐私的话题&#xff0c;不说全部&#xff0c;应该大多数程序员都有过想要接私活的想法&#xff0c;当然&#xff0c;也有部分得道成仙的不主张接私活。但是很少有人在公开场合讨论私活的问题&#xff0c;似乎都在避嫌。就跟有人下班后…

2023河南萌新联赛第(五)场:郑州轻工业大学

A.买爱心气球 原题链接 : 登录—专业IT笔试面试备考平台_牛客网 博弈论 : #include <iostream> using namespace std; int t,n,m; string s1 "Alice",s2 "Bob"; int main() {cin>>t;while(t--){cin>>n>>m;if (n % 3 0) {cou…

简单介绍 CPU 的工作原理

内部架构 CPU 的根本任务就是执行指令&#xff0c;对计算机来说最终都是一串由 0 和 1 组成的序列。CPU 从逻辑上可以划分成 3 个模块&#xff0c;分别是控制单元、运算单元和存储单元 。其内部架构如下&#xff1a; 【1】控制单元 控制单元是整个CPU的指挥控制中心&#xff…