聊聊ThreadLocal(一)

news2024/11/27 14:28:16
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

话说《中华英雄》有一个情节就是华英雄远赴美国,结果被卖到采石场做苦力。后来联合鬼仆师兄还有采石场的其他朋友,大闹了一场。所以本篇文章开头,打算自己画个漫画,纪念一下逝去的童年时光:

咳咳,扯远了。今天我们来聊聊ThreadLocal。至于这个漫画,自有我的用意。

内容介绍:

  • ThreadLocal初印象
  • 如何调戏ThreadLocalMap
  • ThreadLocal简单使用
  • ThreadLocal源码解析
  • ThreadLocal、ThreadLocalMap、Thread三者关系
  • 彩蛋
  • 待扩展内容

ThreadLocal初印象

网上已经有很多ThreadLocal相关的博文,我个人获益颇多,但还是不够直观。尤其对于“一个线程可以有多个threadLocal”的说法,有那么一段时间让我感到很困惑…

自学JavaWeb时,在JDBC一章崔老师曾引入ThreadLocal实现对connection对象的管理。按崔老师的说法,可以将ThreadLocal理解为一个大Map,key是每个线程,value是Object类型。每个线程访问该Map时,只能取到与本线程绑定的变量,从而做到线程隔离。

虽然这个说法不是很准确,但还是非常直观的。而且据说在ThreadLocal早期版本中,确实是这样实现的:ThreadLocal内部塞了一个Map,以线程作为key。ThreadLocal本身不存东西。

但不知道从哪一版开始,ThreadLocal的实现已经做了修改。从JDK1.8的源码来看,ThreadLocalMap的key不再是线程,而是ThreadLocal对象。

你肯定很好奇,以前用线程作为key,每个线程访问Map得到与自己绑定的value,很合理。现在改用ThreadLocal对象作为key,每个线程如何知道哪个键值对属于自己?这里先按下不表。

为了让大家对ThreadLocal的内部实现有个快速、直观的认识,我画了一张图:

ThreadLocal的静态内部类:ThreadLocalMap。而Thread中有个成员变量threadLocals可以指向它

为了方便理解,可以把ThreadLocal看成是一个工具箱,里面提供了一系列操作容器(ThreadLocalMap)的方法:get、set、remove...


如何调戏ThreadLocalMap

看到这里,我们已经知道ThreadLocal之所以能存东西,是因为里面有个ThreadLocalMap。那如果我们能直接得到ThreadLocalMap实例,就能撇开ThreadLocal自己玩了。没有中间商赚差价,岂不妙哉?

通过阅读源码,我们发现ThreadLocalMap是ThreadLocal的内部类,而且是静态内部类。这就非常easy了啊。想当年我们学JavaSE的时候,内部类也没少玩。先自己试试看:

我们发现,可以直接通过 new Outer.Inner()方式实例化静态内部类,稳得一批。真开心,终于可以不理会ThreadLocal,自己单干了:

结果发现压根不行...

看了错误提示才恍然大悟:ThreadLocal虽然是public权限,但是静态内部类ThreadLocalMap只是默认权限。如果一个包下的类想要供其他包的类使用,那么这个类必须是public,不论是普通类还是内部类。很遗憾,ThreadLocalMap并不是:

既不能通过外部类实例化ThreadLocalMap,又无法直接new ThreadLocalMap():

那么,ThreadLocalMap对于我们来说,就是完全限制访问的,只能当它不存在...

也就说,ThreadLocal通过给ThreadLocalMap使用默认的权限修饰符,使得ThreadLocalMap无法被其他包的类引用,最终将ThreadLocalMap完美地隐藏在java.lang包内部。

所以结论是,我们无法直接调戏ThreadLocalMap,只能通过ThreadLocal明媒正娶。


ThreadLocal简单使用

放弃不正规的渠道,接下来看一下ThreadLocal的正经用法:

public class TestThreadLocal {
        //创建两个ThreadLocal实例并指定泛型,分别存储Long/String类型数据
	private static ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
	private static ThreadLocal<String> stringLocal = new ThreadLocal<String>();
        
        //set方法,因为只是内部调用,用了private
	private void set() {
		longLocal.set(Thread.currentThread().getId());
		stringLocal.set(Thread.currentThread().getName());
	}

        //get方法
	private long getLong() {
		return longLocal.get();
	}
        
        //get方法
	private String getString() {
		return stringLocal.get();
	}

	public static void main(String[] args) throws InterruptedException {
               //------main线程执行开始--------
		final TestThreadLocal test = new TestThreadLocal();
                
		test.set();
		System.out.println(test.getLong());
		System.out.println(test.getString());

		Thread thread = new Thread() {
			public void run() {
                                //-------Thread-0线程执行开始--------
				test.set();
				System.out.println(test.getLong());
				System.out.println(test.getString());
                                //-------Thread-0线程执行结束--------
			}
		};
		thread.start();
		//thread.join():用来指定当前主线程等待其他线程执行完毕后,再来继续执行Thread.join()后面的代码
		thread.join();
		System.out.println(test.getLong());
		System.out.println(test.getString());
                //------main线程执行结束--------
 	}
}

两个线程执行示意图:main,Thread-0

输出结果:

main线程两次打印的中途,Thread-0线程开启并调用了test.set()进行设置。main线程和Thread-0设置的值肯定不同,但最终main线程前后打印结果一致。也就是说,main线程和Thread-0是线程隔离的,变量相互独立。


ThreadLocal源码分析

ThreadLocal为什么能做到线程隔离呢?我们来看一下完整的类结构:

左边是工具(ThreadLocal),右边是容器(ThreadLocalMap)

看了上面的类结构后,我们回顾一下上面看过的图:

这次感觉亲切多了吧?

虽然ThreadLocal提供的方法很多,但常用的大部分方法会在get()和set()中被调用,所以我们只分析这两个方法。

另外,请注意,内部类其实本质上和普通类差不多,内部类实例和外部类实例之间也并不存在继承。只不过ThreadLocalMap的情况稍微特殊一些,由于权限问题,我们必须通过ThreadLocal间接操作它。

所以稍后画示意图时,我更倾向于把TheadLocalMap单独抽出来,画成下面这样:

ThreadLocalMap内部也有个静态内部类:Entry,用来装键值对

set源码图解

其实就是华英雄向包工头要箩筐的代码实现。包工头给了华英雄一个箩筐,华英雄放了(set)一块砖进去。整体比较简单,有几点注意一下即可:

1.线程对象刚创建时,threadLocals肯定还未赋值,所以是null

2.在ThreadLocal的set()中,调用getMap(currentThread)得到当前线程的threadLocals。如果发现当前线程尚未绑定ThreadLocalMap实例,ThreadLocal会创建一个Map并绑定。此时,Thread中的threadLocals指向新创建的ThreadLocalMap实例

3.ThreadLocalMap创建的table可以看成一个哈希表,默认大小是16,即有16个槽(slot)。创建table完毕,根据firstKey算出本次插入的槽位,然后用内部类Entry将两个值包装成键值对(entry),放入槽中:table[i] = new Entry(firstKey, firstValue);

get源码图解

上面是华英雄第二次访问包工头(ThreadLocal)的代码实现。包工头发现他已经有箩筐(ThreadLocalMap),所以不再分配新的箩筐,于是华英雄找到自己的箩筐,拿到了之前set进去的砖头。

但这是非常理想化的场景。现在我们来设想一下:倘若在set之前,先get,会发生什么呢?

会有以下两种可能:

1.ThreadLocalMap还未初始化:箩筐都没有,如何得到砖?

做了三件事:

  • 创建map
  • 给map设置一个键值对{threadLocal : initialValue}
  • 返回initialValue,默认null

2.ThreadLocalMap已经初始化,但是map中没有查到这个key:有箩筐,但是没找到想要的那块砖

做了两件事:

  • 往map里设置键值对{threadLocal : initialValue}
  • 返回initialValue,默认null

set之后get,会得到刚才set的值。而在set之前就get会产生两种情况,但两种情况唯一的差异在于是否创建map,共同点则是:不管新Map还是旧Map,由于之前没有set值,所以此次get肯定是取不到值的。但总要给个返回结果吧?ThreadLocalMap的做法是往Map中插入键值对{this ThreadLocal : initialValue},然后返回initialValue。也就是说,取不到值就统一返回默认值。

为什么不直接返回默认值,还要多加一步插入entry的操作?因为这样下次你就能找到值了…

但是要注意,initialValue默认是null:

如果我们后续还有操作,可能会发生空指针异常,所以推荐创建ThreadLocal对象时,复写initialValue():


ThreadLocal、ThreadLocalMap、Thread三者关系

我知道,上面的源码分析未必能让大家对ThreadLocal有个全局的认识。因为Thread/ThreadLocal/ThreadLocalMap的关系实在太乱了。接下来做一下整理:

1.虽然ThreadLocalMap是ThreadLocal的静态内部类,但它们的实例对象并不存在继承或者包裹关系。完全可以当成两个独立的实例。

2.ThreadLocal的作用有两个

  • 工具类,提供一系列方法操作ThreadLocalMap,比如get/set/remove
  • 隔离Thread和ThreadLocalMap,防止程序员直接创建ThreadLocalMap(无法调戏)。但自身的get/set内部会判断当前线程是否已经绑定一个ThreadLocalMap。有就继续用,没有就为其绑定

现在,让我们回到华英雄的故事。

为了防止工人随意占用箩筐(ThreadLocalMap),采石场的箩筐统一交给包工头(ThreadLocal)管理(设计成内部类且不给public权限)。虽然箩筐在包工头手里,但是分发给工人(Thread)后,这个箩筐就和工人绑定了,和包工头没太大关系。

所以本质上,ThreadLocal和Thread没有必然联系。哪怕再来几个工人,只要他确实还没有箩筐,包工头都会给他一个。

另外,采石场那么多工人,包工头是不会去记自己的箩筐给过哪位工人的。但工人每次去访问包工头时,包工头都会问他是否已经有箩筐,有的话就用自己现有的箩筐搬石头。至于工人现有的箩筐是不是自己当初发的,重要吗?

所以回到文章开头我困惑的那句:一个线程可以有多个threadLocal。就会发现这句话,好像有道理,又好像完全没道理。因为它俩并不存在“谁拥有谁”的关系。实在要说的话,应该是一个工人(Thread)只能有一个箩筐(ThreadLocalMap)。


现在,把一开始的程序示意图画一遍:


彩蛋

最后,还有个小彩蛋,由于不知道放哪,就放这儿了。

我们在调戏ThreadLocalMap时,发现外部无法直接创建它。但是后面分析源码时,我们发现ThreadLocal都是调用createMap()创建的。所以,贼心不死的我想看看是否可以直接通过threadLocal.createMap()创建:

错误提示:非public的method无法被不同包下的类调用...和内部类的权限问题一样。

ThreadLocal虽然设计了createMap(),但并没打算给外部调用。所以并没有给createMap()加public。

而是通过对外暴露public void set()和public T get(),并在方法内加入判断,使得在满足条件时才能为线程创建ThreadLocalMap实例。

答应我,放弃调戏ThreadLocalMap!!!

(不愧是JDK源码,设计得真好...)


待扩展内容

上文的replaceStaleEntry()继续往下分析,会发现ThreadLocalMap本身有清除“废弃槽”的机制。所谓“废弃槽”,是我自己乱翻译的:比如某个ThreadLocal对象已经被回收,那么key = null,对应的value再也用不了。这种“废弃槽”多了以后,会浪费内存,甚至造成内存溢出。

另外,Entry继承了WeakReference,将自己的key包装为弱引用。

---------------------------------------------------------------------------------------------------------------------------------

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
进群,大家一起学习,一起进步,一起对抗互联网寒冬

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

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

相关文章

【文献阅读】Low Cost Edge Sensing for High Quality Demosaicking

[PDF] [Code] 1. 摘要 使用彩色滤光器阵列(CFA)的数码相机需要一个去马赛克程序来形成完整的RGB图像。对于数码相机行业来说&#xff0c;解调速度和解调精度同样重要&#xff0c;因为相机用户已经习惯了即时查看拍摄的照片。此外&#xff0c;与去马赛克相关的成本不应超过使用…

OpenCV入门5——OpenCV的算术与位运算

文章目录 图像的加法运算图像的减法运算图像的乘除运算图像的融合OpenCV位运算-非操作OpenCV位操作-与运算OpenCV位操作-或与异或为图像添加水印 图像的加法运算 # -*- coding: utf-8 -*- import cv2 import numpy as npimg cv2.imread(E://pic//4.jpg)# 图的加法运算就是矩阵…

3ds max 2024 V-Ray 6 ACES workflow 工作流设置

ACES的流程包括2个设置&#xff1a; 1、环境设置&#xff1b;2、贴图设置&#xff1a; 一、环境设置&#xff1a;3ds max 2024已经内置了OCIO文件&#xff1b;设置一下即可&#xff1b; 二、贴图设置&#xff1a; 所有类型贴图加载有默认和加后缀2种方法&#xff1a; 第一…

提高Producer的发送速度

发送一条消息出去要经过三步&#xff0c;一是客户端发送请求到服务器&#xff0c;二是服务器处理该请求&#xff0c;三是服务器向客户端返回应答&#xff0c;一次消息的发送耗时是上述三个步骤的总和。在一些对速度要求高&#xff0c;但是可靠性要求不高的场景下&#xff0c;比…

Python装饰器的用途和实例

说明 装饰器是Python中非常有用的工具&#xff0c;它们可以用于修改或扩展函数或类的行为&#xff0c;而无需修改其原始定义。装饰器通常是一个函数&#xff0c;它接受一个函数作为参数&#xff0c;并返回一个新的函数或类。下面我们将介绍一些常见的装饰器用途和示例。 记录日…

SAP Dump:RFC callback call rejected by positive list

问题描述:配置SM59 连接时报错CALLBACK_REJECTED_BY_WHITELIST 报错简介: RFC callback call rejected by positive list 报错内容: An RFC callback has been prevented due to no corresponding positive list entry being configured. The original RFC called function m…

ceph学习笔记

ceph ceph osd lspoolsrbd ls -p testpool#查看 ceph 集群中有多少个 pool,并且每个 pool 容量及利 用情况 rados dfceph -sceph osd tree ceph dfceph versionsceph osd pool lsceph osd crush rule dumpceph auth print-key client.adminceph orch host lsceph crash lsceph…

vue2、vue3傻傻分不清楚

前端工作也做了几年了&#xff0c;好多东西还是分不清楚。特别是对于最基础的书写格式还是有点模糊不清&#xff0c;今天特地整理一下&#xff0c;以供日后查阅。 vue2 //最纯洁简单的一种创建组件的方式&#xff0c;使用基础 Vue 构造器&#xff0c;创建一个“子类”。 <…

2.认识中台

1.数字化中台初步认识与建设策略 中台的定义 阿里对中台的定义: 中台是一个基础的理念和架构&#xff0c;我们要用中台的思想建设、联通所有基础服务&#xff0c;共同支持上端的业务。业务中台更多的是支持在线业务&#xff0c;数据中台则提供基础数据处理能力和很多的数据产…

基于SDN技术构建多平面业务承载网络

随着企业数字化的浪潮席卷各个行业&#xff0c;传统网络架构面临着更为复杂和多样化的挑战。企业正在寻找一种全面适应数字化需求的网络解决方案。随着软件定义网络&#xff08;SDN&#xff09;的发展&#xff0c;“多业务SDN一张网”解决方案为企业提供了一种全新的网络架构&a…

解决requests 2.28.x版本SSL错误:证书验证失败

1、问题背景 在使用requests 2.28.1版本时&#xff0c;我进行HTTP post传输报告负载时&#xff0c;由于SSL验证设置为True&#xff0c;请求失败&#xff0c;错误如下&#xff1a;(Caused by SSLError(SSLCertVerificationError(1, ‘[SSL: CERTIFICATE_VERIFY_FAILED] certifi…

Java进阶笔记(面向对象后, 持续更新)

常用API 游戏打包成exe 考虑的因素 要有图形化界面代码要打包起来游戏用到的图片也要打包JDK也要打包 核心步骤 把所有代码打包成一个压缩包, jar后缀的压缩包把jar包转换成exe安装包把第二部的exe, 图片, JDK整合在一起, 变成最终的exe安装包 1. Math 是一个帮助我们用…

人脸106和240点位检测解决方案

人脸识别技术已经深入到我们生活的各个领域&#xff0c;从手机解锁、门禁系统到视频娱乐化等&#xff0c;都离不开高精度的人脸关键点检测。美摄科技作为行业的领军企业&#xff0c;一直致力于提供最先进、最稳定的人脸识别技术&#xff0c;近日&#xff0c;我们推出了全新的10…

阿桂天山的小工具:我将16个Excel文件中31万多条数据拆分成318个文件

1.话不多说,先上图看效果 2.技术说明及实用源码 2.1)pythonflaskpandas , 由于我的开发环境版本问题,为了能读xls,xlsx,但又不想升级,只能通过xlrd 1.2.0读取xls,xlsx文件再转换成dict字典格式,再通过 data pd.DataFrame(dict_data)实现类型转换 2.2)实用代码,保证不丢任何一行…

深入探讨TensorFlow:张量与矩阵

在机器学习和深度学习领域中&#xff0c;TensorFlow作为一款强大且受欢迎的开源机器学习框架&#xff0c;为研究人员和开发者提供了丰富的工具和资源。在TensorFlow中&#xff0c;张量&#xff08;tensor&#xff09;和矩阵&#xff08;matrix&#xff09;是核心概念&#xff0…

【vue】下载导出excel

下载导出excel 首先使用的tdesign框架&#xff0c;要导出后端返回的数据流excel 遇见的问题 下载的文件&#xff0c;里边的内容是undefined 观察报错 一看就知道并不是后端的报错&#xff0c;后端不可能是undefined 在强烈的好奇心驱动下&#xff0c;看了下接口&#xff0…

Python实现Tom与Jerry

文章目录 写在前面系列文章绘图基础Tom&Jerry写在后面 写在前面 汤姆永远抓不到杰瑞&#xff0c;我们的故事永远不会结束&#xff01;本期博主给大家带来了Tom&Jerry&#xff0c;快来看看吧~&#xff08;Tom和Jerry可以修改成自己想修改的名字哦&#xff09; 系列文章…

解决SSH连接自动断开的问题

一、环境 centos7.6 xshell7 二、目标 ssh长联状态&#xff0c;不主动断开 三、实施 1.修改/etc/profile文件 末尾添加export TMOUT0 vim /etc/profileexport TMOUT02.修改/etc/ssh/sshd_config文件 默认都被注释掉&#xff0c;放开并设置CAI参数为0-60间 vim /etc…

轻松一刻|Walrus CLI与CI/CD工具集成,轻松部署2048游戏

Walrus 是一款开源的基于平台工程理念、以应用为中心、以完整应用系统自动化编排交付为目标进行设计开发的云原生应用平台&#xff0c;简化和自动化应用部署与发布流程并与现有的 CI/CD 流水线无缝集成。今天我们来点有趣的&#xff0c;跟随本教程&#xff0c;一起将 Walrus CL…

2023年第九届数维杯国际大学生数学建模挑战赛A题

2023年第九届数维杯国际大学生数学建模挑战赛正在火热进行&#xff0c;小云学长又在第一时间给大家带来最全最完整的思路代码解析&#xff01;&#xff01;&#xff01; A题思路解析如下&#xff1a; 完整版解题过程及代码&#xff0c;稍后继续给大家分享~ 更多题目完整解析点…