ThreadLocal(超详细介绍!!)

news2024/12/27 11:41:25

关于ThreadLocal,可能很多同学在学习Java的并发编程部分时,都有所耳闻,但是如果要仔细问ThreadLocal是个啥,我们可能也说不清楚,所以这篇博客旨在帮助大家了解ThreadLocal到底是个啥?

1.ThreadLocal是什么?

首先,我们要知道的是,ThreadLocal类位于Java标准库的java.lang包中,它是Java中的一个类,我们可以用它来声明一个ThreadLocal变量,如下:

ThreadLocal<String> local = new ThreadLocal<>();

好的,接下来解释一下ThreadLocal的含义:

它从名字上看,叫做本地线程变量,意思是说,ThreadLocal中填充的的是当前线程的变量,该变量对其他线程而言是封闭且隔离的,ThreadLocal为变量在每个线程中创建了一个副本,这样每个线程都可以访问自己内部的副本变量。

相信上面的文字描述大家会不太理解,简单来说,就是用ThreadLocal创建的变量,我们可能会在不同的线程中用到,那么为了避免线程安全问题,每个线程都会为自己单独存一份这个变量,并且单独使用和修改这个变量,这样不同的线程之间就各自使用各自的ThreadLocal变量,互不影响。

但是到底具体每个线程是怎样存储的这个变量,以及这个变量如何被这个线程调用,这些的底层是如何实现的,请接着向下看。

2.举例

大家先看一个例子:

public class ThreadLocalTest02 {

    public static void main(String[] args) {
//创建一个ThreadLocal变量,名为local
        ThreadLocal<String> local = new ThreadLocal<>();
//创建10个线程,并且每次都在不同的线程中加入这个local变量
        IntStream.range(0, 10).forEach(i -> new Thread(() -> {
//使用set方法设置加入的local内容
            local.set(Thread.currentThread().getName() + ":" + i);
//然后输入当前线程存储的local变量的信息
            System.out.println("线程:" + Thread.currentThread().getName() + ",local:" + local.get());
        }).start());
    }
}

结果如下:

输出结果:
线程:Thread-0,local:Thread-0:0
线程:Thread-1,local:Thread-1:1
线程:Thread-2,local:Thread-2:2
线程:Thread-3,local:Thread-3:3
线程:Thread-4,local:Thread-4:4
线程:Thread-5,local:Thread-5:5
线程:Thread-6,local:Thread-6:6
线程:Thread-7,local:Thread-7:7
线程:Thread-8,local:Thread-8:8
线程:Thread-9,local:Thread-9:9

上面的结果说明了什么呢?我们在每个线程中都添加了local对象,并且内容是不同的,然后我们再使用get方法输出local的值。我们发现,我们只使用了一个local对象,但是在十个线程中的值都是不同的,而且它们的值不会相互影响,这就是ThreadLocal的简单应用。不同的线程对这个local对象有着自己的备份。

3.Set方法

请大家先仔细阅读一下下面这段源码,逻辑一点也不难,我加了注释:

public class ThreadLocal<T> {

    public void set(T value) {
//先获取当前线程,例如在线程1中调用了local.set方法,那么这个t就是线程1
        Thread t = Thread.currentThread();

//然后获取当前线程1中的ThreadLocalMap
        ThreadLocalMap map = getMap(t);


//如果map为空,说明此线程还没有存入任何一个ThreadLocal对象,我们就创建一个ThreadLocalMap
//如果map不为空,那么我们就直接将value存入这个ThreadLocalMap中
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

大家现在可能会有疑惑,什么是ThreadLocalMap啊?为啥是从当前线程中获取啊?还有createMap方法,到底是干啥的捏?我们一一进行解释:

什么是ThreadLocalMap?

我们刚才上面说,我们每个线程都会存储ThreadLocal对象的备份,那么存储在哪里呢,答案就是在ThreadLocalMap中,ThreadLocalMap为 ThreadLocal的一个静态内部类,里面定义了Entry来保存数据,那么既然是map,就会有键值对的结构,键的位置存的就是我们的ThreadLocal对象,而值存储的就是通过set方法存入的那个值,例如这一句代码:local.set( i);那么存到这个线程中的ThreadLocalMap的一个entry中,键和值就分别是 local:i

接下来我们就看一下ThreadLocalMap(它是ThreadLocal的一个内部类,还有Entry的结构:

  // 内部类ThreadLocalMap
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            // 内部类Entry,实际存储数据的地方
            // Entry的key是ThreadLocal对象,不是当前线程ID或者名称
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        // 注意这里维护的是Entry数组
        private Entry[] table;
    }

可以看出实际上存储数据的是Entry,而TheadLocalMap则是一个Entry数组;

ok,了解了这个结构后,我们又回到宏观的角度看待一下问题,刚才说每个线程都有自己的备份,并且这些备份是当前线程独有的,那么既然上面说存储数据的是ThreadLocalMap,并且每个线程都有自己的独有的一份,那么这个ThreadLocalMap到底存在哪里呢?答案就是:ThreadLocalMap是作为Thread类的一个私有属性实现的,这样就可以保证每个Thread线程都有自己独一份的TheadLocalMap来存储自己的Threadlocal变量。

public class Thread {
    /* ... 省略其他代码 ... */

    /**
     * ThreadLocalMap实例,用于存储ThreadLocal变量的键值对
     */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /* ... 省略其他代码 ... */
}

 好好好,现在我们算是知道了,原来为每个线程存储这些ThreadLocal变量的,就是Thread类中的属性threadLocals 

那么这样我们就能解释为什么要从线程中获取map了,看一下刚才的set方法中的这一句,我们就知道为啥要从线程中获取了:

//然后获取当前线程1中的ThreadLocalMap
        ThreadLocalMap map = getMap(t);

接着就是后面的代码,相信大家也就能明白为什么要这样写啦:

//如果map为空,说明此线程还没有存入任何一个ThreadLocal对象,我们就创建一个ThreadLocalMap
//如果map不为空,那么我们就直接将value存入这个ThreadLocalMap中
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

这是createMap方法,看到它给什么赋值吗,就是我们刚才说的Thread线程类中的那个存储ThreadLocalMap的属性哦~

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

ok,set方法就介绍到这里了

4.get方法

get方法的作用很简单,它通过local对象调用,返回当前线程的以local对象为键,对应的那个值即可;例如 System.out.println(local.get());就是输出当前线程的local对象当时通过set方法存入的值。

get方法的源码如下:

    public T get() {
//先获取当前调用get方法的线程
        Thread t = Thread.currentThread();

//然后获取此线程的ThreadLocalMap对象,这里面存储着local键值对
        ThreadLocalMap map = getMap(t);

/*如果map不为空,就在map里面寻找键为this的entry,为什么是this呢,因为当前类
是ThreadLocal类,而get方法通过local.get()的方式调用,所以这里的this就指的
是这个local对象,也就是entry的键。如果找的了这个以local为键的entry,我们就
返回对应的值即可。
*/
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

ok,ThreadLocal的具体应用和get方法就介绍到这里

5.ThreadLocal的结构

有了上面的基础,我们现在来看一下他在内存中的结构:

6.内存泄漏问题

仔细看下ThreadLocal内存结构就会发现,Entry数组对象通过ThreadLocalMap最终被Thread持有,并且是强引用。也就是说Entry数组对象的生命周期和当前线程一样。即使ThreadLocal对象被回收了,Entry数组对象也不一定被回收,这样就有可能发生内存泄漏。ThreadLocal在设计的时候就提供了一些补救措施:

  • Entry的key是弱引用的ThreadLocal对象,很容易被回收,导致key为null(但是value不为null)。所以在调用get()、set(T)、remove()等方法的时候,会自动清理key为null的Entity。
  • remove()方法就是用来清理无用对象,防止内存泄漏的。所以每次用完ThreadLocal后需要手动remove()。

解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。

如同 lock 的操作最后要执行解锁操作一样,ThreadLocal使用完毕一定记得执行remove 方法,清除当前线程的数值。如果不remove 当前线程对应的VALUE ,就会一直存在这个值。

这里复习一下对象的强引用、软引用、弱引用

1.强引用

我们平日里面的用到的new了一个对象就是强引用,例如 Object obj = new Object();当JVM的内存空间不足时,宁愿抛出OutOfMemoryError使得程序异常终止也不愿意回收具有强引用的存活着的对象!

2.软引用

当JVM认为内存空间不足时,就回去试图回收软引用指向的对象,也就是说在JVM抛出OutOfMemoryError之前,会去清理软引用对象。

3.弱引用

在GC的时候,不管内存空间足不足都会回收这个对象,同样也可以配合ReferenceQueue 使用,也同样适用于内存敏感的缓存。ThreadLocal中的key就用到了弱引用。

7.最后我们还要知道为什么要使用ThreadLocal?

ThreadLocal类在多线程编程中有几个重要的用途和优势:

  1. 线程隔离:ThreadLocal提供了一种将数据与线程关联的机制。通过使用ThreadLocal,可以为每个线程创建独立的变量副本,使得每个线程都可以独立地访问和修改自己的变量副本,而不会干扰其他线程的数据。这样可以实现线程间的数据隔离,避免了线程安全问题。

  2. 状态传递:ThreadLocal可以用于在同一个线程的多个方法之间传递状态信息,而无需在方法参数中显式传递。通过将状态信息存储在ThreadLocal变量中,不同的方法可以通过ThreadLocal访问和修改共享的状态,而无需显式传递参数。这样可以简化方法的调用,提高代码的可读性和可维护性。

  3. 线程上下文管理:有些情况下,需要在整个线程执行期间共享某些上下文信息,比如用户认证信息、数据库连接等。通过将这些信息存储在ThreadLocal中,可以在同一线程的任何地方方便地访问和使用这些信息,而无需显式传递或在每个方法中重复获取。

  4. 避免锁竞争:在某些情况下,使用ThreadLocal可以避免使用锁来同步对共享变量的访问。由于每个线程都有自己的变量副本,线程之间不会产生竞争条件,从而避免了锁竞争和同步开销,提高了程序的性能。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AgentBench::AI智能体发展的潜在问题一

从历史上看,几乎每一种新技术的广泛应用都会在带来新机遇的同时引发很多新问题,AI智能体也不例外。从目前的发展看,AI智能体的发展可能带来的新问题可能包括如下方面: 第一是它可能带来涉及个人数据、隐私,以及知识产权的法律纠纷的大幅增长。要产生一个优秀的AI智能体,除…

ZLMediakit编译(Win32)

ZLMediakit编译流程&#xff0c;本文是编译32位的ZLMediakit 下载OpenSSL 直接下载binary就好了&#xff0c;地址&#xff1a;https://slproweb.com/download/Win32OpenSSL-1_1_1u.msi 也可以根据自己的需求下载其他版本&#xff0c;地址https://slproweb.com/products/Win32…

[oneAPI] 手写数字识别-LSTM

[oneAPI] 手写数字识别-LSTM 手写数字识别参数与包加载数据模型训练过程结果 oneAPI 比赛&#xff1a;https://marketing.csdn.net/p/f3e44fbfe46c465f4d9d6c23e38e0517 Intel DevCloud for oneAPI&#xff1a;https://devcloud.intel.com/oneapi/get_started/aiAnalyticsToolk…

【C语言】自定义实现strcpy函数

大家好&#xff0c;我是苏貝&#xff0c;本篇博客带大家了解如何自定义实现strcpy函数&#xff0c;如果你觉得我写的还不错的话&#xff0c;可以给我一个赞&#x1f44d;吗&#xff0c;感谢❤️ 一. 了解strcpy函数。 函数原型&#xff1a;char* strcpy( char* destination , …

LLM - 大模型评估指标之 BLEU

目录 一.引言 二.BLEU 简介 1.Simple BLEU 2.Modified BLEU 3.Modified n-gram precision 4.Sentence brevity penalty 三.BLEU 计算 1.计算句子与单个 reference 2.计算句子与多个 reference 四.总结 一.引言 机器翻译的人工评价广泛而昂贵&#xff0c;且人工评估可…

【uni-app报错】获取用户收货地址uni.chooseAddress()报错问题

chooseAddress:fail the api need to be declared in …e requiredPrivateInf 原因&#xff1a; 小程序配置 / 全局配置 (qq.com) 解决&#xff1a; 登录小程序后台申请接口 按照流程申请即可 在项目根目录中找到 manifest.json 文件&#xff0c;在左侧导航栏选择源码视图&a…

代码pytorch-adda-master跑通记录

前言 最近在学习迁移学习&#xff0c;ADDA算法&#xff0c;由于嫌自己写麻烦&#xff0c;准备先跑通别人的代码。 代码名称&#xff1a;pytorch-adda-master 博客&#xff1a;https://www.cnblogs.com/BlairGrowing/p/17020378.html github地址&#xff1a;https://github.com…

Vue3 —— watchEffect 高级侦听器

该文章是在学习 小满vue3 课程的随堂记录示例均采用 <script setup>&#xff0c;且包含 typescript 的基础用法 前言 Vue3 中新增了一种特殊的监听器 watchEffect&#xff0c;它的类型是&#xff1a; function watchEffect(effect: (onCleanup: OnCleanup) > void,o…

第三届“赣政杯”网络安全大赛 | 赛宁筑牢安全应急防线

​​为持续强化江西省党政机关网络安全风险防范意识&#xff0c;提高信息化岗位从业人员基础技能&#xff0c;提升应对网络安全风险处置能力。由江西省委网信办、江西省发展改革委主办&#xff0c;江西省大数据中心、国家计算机网络与信息安全管理中心江西分中心承办&#xff0…

【负荷频率和电压控制】电力系统的组合负荷频率和电压控制模型研究(Simulink)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

CXL 寄存器介绍 (1) - 寄存器分类

&#x1f525;点击查看精选 CXL 系列文章&#x1f525; &#x1f525;点击进入【芯片设计验证】社区&#xff0c;查看更多精彩内容&#x1f525; &#x1f4e2; 声明&#xff1a; &#x1f96d; 作者主页&#xff1a;【MangoPapa的CSDN主页】。⚠️ 本文首发于CSDN&#xff0c…

【贪心】CF1822 E

Problem - 1822E - Codeforces 题意&#xff1a; 思路&#xff1a; 简单复盘一下思路 首先&#xff0c;n为奇数或有一种字符出现次数> n / 2就无解的结论是可以根据样例看出来的 然后就显然的发现&#xff0c;每2个不同的回文对有1的贡献 那么这样匹配之后会有剩余的回文…

JVM - 垃圾回收机制

JVM的垃圾回收机制(简称GC) JVM的垃圾回收机制非常强大&#xff0c;是JVM的一个很重要的功能&#xff0c;而且这也是跟对象实例息息相关的&#xff0c;如果对象实例不用了要怎么清除呢&#xff1f; 如何判断对象已经没用了 当JVM认为一个对像已经没用了&#xff0c;就会把这个…

【C++STL基础入门】string类的基础使用

文章目录 前言一、STL使用概述二、string类概述1.string类的构造函数string输出示例代码 2.string类属性属性是什么属性API示例代码 3.输出输出全部输出单个字符 总结 前言 本系列文章使用VS2022&#xff0c;C20版本 STL&#xff08;Standard Template Library&#xff09;是…

linux平台实现虚拟磁盘驱动(通用的块设备驱动和基于SCSI的磁盘驱动)

by fanxiushu 2023-08-16 转载或引用请桌面原始作者。 实现linux平台的虚拟磁盘驱动&#xff0c;是为了要实现在linux远程无盘启动的。 linux平台下的无盘启动&#xff0c;现成的办法有许多&#xff0c;比如iSCSI&#xff0c;NFS&#xff0c;NBD等都可以&#xff0c; 不过我都没…

JVM中释放内存的三种方法

判断是否需要垃圾回收可以采用分析。 1标记--清除算法 分为两个阶段&#xff0c;标记和清除&#xff0c;先利用可达性分型标记还存活的对象&#xff0c;之后将没有被标记的对象删除&#xff0c;这样容易生成空间碎片&#xff0c;而且效率不稳定 标记阶段&#xff1a; 标记阶段…

C#和Java的大端位和小端位的问题

C#代码里就是小端序,Java代码里就是大端序&#xff0c; 大端位:big endian,是指数据的高字节保存在内存的低地址中&#xff0c;而数据的低字节保存在内存的高地址中&#xff0c;也叫高尾端 小端位:little endian,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存…

vue : 无法加载文件 F:\nodejs\vue.ps1,因为在此系统上禁止运行脚本。

报错信息如下 在使用Windows PowerShell输入指令查看版本时或者用脚手架创建vue项目时都有可能报错&#xff0c;报错信息如下&#xff1a;vue : 无法加载文件 F:\nodejs\vue.ps1&#xff0c;因为在此系统上禁止运行脚本 解决方案&#xff1a; 原因&#xff1a;因为Windows Po…

问道管理:金叉死叉十句口诀?

随着越来越多人参加股票买卖&#xff0c;关于股票商场的了解也变得越来越重要。其中一项重要的概念就是金叉死叉&#xff0c;这是一种均线穿插的现象&#xff0c;而均线穿插是技能剖析的重点之一。在本文中&#xff0c;咱们将会从多个角度剖析金叉死叉&#xff0c;并给出十句口…

Qt应用开发(基础篇)——MDI窗口 QMdiArea QMdiSubWindow

一、前言 QMdiArea类继承于QAbstractScrollArea&#xff0c;QAbstractScrollArea继承于QFrame&#xff0c;是Qt用来显示MDI窗口的部件。 滚屏区域基类 QAbstractScrollAreahttps://blog.csdn.net/u014491932/article/details/132245486 框架类 QFramehttps://blog.csdn.net/u01…