ThreadLocal(超详细介绍!!)

关于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变量,名为localThreadLocal<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就是线程1Thread t = Thread.currentThread();//然后获取当前线程1中的ThreadLocalMapThreadLocalMap map = getMap(t);//如果map为空,说明此线程还没有存入任何一个ThreadLocal对象,我们就创建一个ThreadLocalMap
//如果map不为空,那么我们就直接将value存入这个ThreadLocalMap中if (map != null)map.set(this, value);elsecreateMap(t, value);}

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

什么是ThreadLocalMap?

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

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

  // 内部类ThreadLocalMapstatic 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中的ThreadLocalMapThreadLocalMap map = getMap(t);

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

//如果map为空,说明此线程还没有存入任何一个ThreadLocal对象,我们就创建一个ThreadLocalMap
//如果map不为空,那么我们就直接将value存入这个ThreadLocalMap中if (map != null)map.set(this, value);elsecreateMap(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.rhkb.cn/news/93720.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

VS2019 + Qt : setToolTip的提示内容出现乱码

VS2019 Qt : setToolTip的提示内容出现乱码 在使用setToolTip()时&#xff0c; setToolTip(QString("asd你好&#xff01;");标签提示只有英文是对的&#xff0c;中文是乱码&#xff01; 应该是编码出了问题。默认情况下&#xff0c;Qt使用的是UTF-8编码&#xf…

Linux学习之sed多行模式

N将下一行加入到模式空间 D删除模式空间中的第一个字符到第一个换行符 P打印模式空间中的第一个字符到第一个换行符 doubleSpace.txt里边的内容如下&#xff1a; goo d man使用下边的命令可以实现把上边对应的内容放到doubleSpace.txt。 echo goo >> doubleSpace.txt e…

Redis系列(一):深入了解Redis数据类型和底层数据结构

Redis有以下几种常用的数据类型&#xff1a; redis数据是如何组织的 为了实现从键到值的快速访问&#xff0c;Redis 使用了一个哈希表来保存所有键值对。 Redis全局哈希表&#xff08;Global Hash Table&#xff09;是指在Redis数据库内部用于存储所有键值对的主要数据结构。…

CC2530实现呼吸灯效果-PWM调光-TIM1定时器使用

目录 一、前言 二、思路及实现方法 三、CC2530相关寄存器 四、思路以及代码实现 五、源码 一、前言 前面我们提到了非定时器模式实现呼吸灯效果&#xff0c;但由于其占用单片机主线程&#xff0c;如果不能加入RTOS的话&#xff0c;很难实现与其他功能的同步使用&#xff0…

C# 随机法求解线性规划问题 蒙特卡洛

线性规划问题: max3x12x2 x12x2<5 2x1x2<4 4x13x2<9 x1>0 x2>0 正确的结果:x11.5; x21, max z6.5 Random random1 new Random(DateTime.Now.Millisecond);Random random2 new Random(DateTime.Now.Millisecond*DateTime.Now.Millisecond);double max-9999,x1…

阿里云服务器镜像大全_Linux和Windows操作系统清单

阿里云服务器操作系统大全&#xff0c;阿里云提供的镜像均为正版授权&#xff0c;正版镜像可以在云服务器ECS上运行的应用程序提供安全、稳定的运行环境系统&#xff0c;阿里云服务器以公共镜像为例分享阿里云服务器操作系统大全&#xff0c;包括Alibaba Cloud Linux镜像、Linu…

大数据面试题:说下Spark中的Transform和Action,为什么Spark要把操作分为Transform和Action?

面试题来源&#xff1a; 《大数据面试题 V4.0》 大数据面试题V3.0&#xff0c;523道题&#xff0c;679页&#xff0c;46w字 可回答&#xff1a;Spark常见的算子介绍一下 参考答案&#xff1a; 我们先来看下Spark算子的作用&#xff1a; 下图描述了Spark在运行转换中通过算…

解密人工智能:线性回归 | 逻辑回归 | SVM

文章目录 1、机器学习算法简介1.1 机器学习算法包含的两个步骤1.2 机器学习算法的分类 2、线性回归算法2.1 线性回归的假设是什么&#xff1f;2.2 如何确定线性回归模型的拟合优度&#xff1f;2.3 如何处理线性回归中的异常值&#xff1f; 3、逻辑回归算法3.1 什么是逻辑函数?…

Redis知识点

Redis 文章目录 RedisNoSQLNoSQL的优势 RedisRedis与其他的Key—value&#xff08;Map&#xff09;相比的区别&#xff1f;关系型数据库和非关系型数据库关系型数据库非关系型数据库 Redis数据类型跳表压缩表String&#xff08;字符串&#xff09;Hash&#xff08;哈希&#xf…

有理数思维导图怎么画?这种方法绘制很方便

有理数思维导图怎么画&#xff1f;为什么要绘制思维导图&#xff1f;因为它可以帮助我们更好地组织和理解信息。在数学中&#xff0c;绘制有理数思维导图也可以帮助我们更好地理解有理数的概念和性质。 有理数是我们日常生活中经常接触到的数学概念&#xff0c;包括正整数、负整…

RabbitMQ的5种消息队列

RabbitMQ的5种消息队列 1、七种模式介绍与应用场景 1.1 简单模式(Hello World) 一个生产者对应一个消费者&#xff0c;RabbitMQ 相当于一个消息代理&#xff0c;负责将 A 的消息转发给 B。 应用场景&#xff1a;将发送的电子邮件放到消息队列&#xff0c;然后邮件服务在队列…

TRT8系列—— 版本差异注意事项

TRT8 一个大版本&#xff0c;8.4-、 8.5、 8.6&#xff08;包含预览功能&#xff09;却有很多变动&#xff0c;一不注意就发现很混乱&#xff0c;特备注此贴。建议具体case可以参考这个合集&#xff0c;真心安利&#xff1a;https://github.com/NVIDIA/trt-samples-for-hackath…

极光笔记 | 如何为您的业务开发和训练一个AI-BOT

生成式AI&#xff08;Generative AI&#xff09;是当今科技领域的前沿技术之一。随着数据量的不断增加和计算能力的不断提升&#xff0c;AI技术在企业和个人生活中的应用越来越广泛。AI-BOT&#xff08;以下简称BOT&#xff09;是生成式AI技术的其中一种重要的应用形式&#xf…

ssh远程连接服务器

一、远程连接服务器简介 二、连接加密技术简介 三、ssh服务配置 四、用户登录ssh服务 Enforcing会强制限制&#xff0c;如端口为22&#xff0c;可以访问&#xff0c;如果是2000端口&#xff0c;不能使用 Permissive是宽容的模式&#xff0c;不限制使用端口 Enforcing会重启失败…

二叉树(ACM版)

【数据结构1-2】二叉树 - 题单 - 洛谷 【数据结构】day2-树_J娇娇_的博客-CSDN博客 上学时的作业 P1827 [USACO3.4] 美国血统 American Heritage 二叉树特点写法&#xff08;非二叉树&#xff09; 截取字符串写法 #include<string> #include<cstring> #include…

git远程仓库的创建及使用

1.仓库的概念&#xff1a; 1.1 本地仓库&#xff1a; 了解远程仓库前我们先了解一下本地仓库&#xff0c;本地仓库开发人员在完成部分代码的编写之后&#xff0c;可以将这一部分的代码做一个提交。这个提交完全就是一个新的版本提交&#xff0c;当然这个提交动作是在开发者的电…

【uniapp】uniapp自动导入自定义组件和设置分包:

文章目录 一、自动导入自定义组件&#xff1a;二、设置分包和预加载&#xff1a; 一、自动导入自定义组件&#xff1a; 【Volar 官网】https://github.com/vuejs/language-tools 二、设置分包和预加载&#xff1a; 【官方文档】https://uniapp.dcloud.net.cn/collocation…

SpringCloud初识

微服务架构4个核心问题&#xff1a; 这四个问题围绕这我们去学的一些东西&#xff0c;是重点!!! 1.服务很多&#xff0c;客户端该如何访问&#xff1f; 2.这么多服务&#xff0c;服务之间该如何通信&#xff1f; 3.这么多服务&#xff0c;该如何治理&#xff1f; 4.服务挂了…

Kafka—工作流程、如何保证消息可靠性

什么是kafka&#xff1f; 分布式事件流平台。希望不仅仅是存储数据&#xff0c;还能够数据存储、数据分析、数据集成等功能。消息队列&#xff08;把数据从一方发给另一方&#xff09;&#xff0c;消息生产好了但是消费方不一定准备好了&#xff08;读写不一致&#xff09;&am…

SSD202D-logo分区添加dtb

SSD202D-kernel-uimage后面加入dtb_旋风旋风的博客-CSDN博客 1.由于内核的uimage老是压缩解压缩,拿到压缩包里面dtb实在困难; 2.把dtb烧在后面又有安全隐患;而且还会有打包升级方法ota之类的很多;又毙掉了, 3.最后直接把dtb放在logo的包里,但是logo包要想添加好,也要深刻的理…