聊聊ThreadLocal(一)

作者简介:大家好,我是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
进群,大家一起学习,一起进步,一起对抗互联网寒冬

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

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

相关文章

【网络】TCP协议的相关实验

TCP协议的相关实验 一、理解listen的第二个参数1、实验现象2、TCP 半连接队列和全连接队列3、关于listen的第二个参数的一些问题4、SYN洪水Ⅰ、什么是SYN洪水攻击Ⅱ、如何解决SYN洪水攻击&#xff1f; 二、使用Wireshark分析TCP通信流程 一、理解listen的第二个参数 在编写TCP…

java“俄罗斯方块”

首先新建议一个包为Tetris &#xff08;俄罗斯方块&#xff09; 类名也叫做Tetris&#xff1b; 代码运行&#xff1a; package Tetris; import java.awt.BorderLayout; import java.awt.Color; import java.awt.GridLayout; import java.awt.event.KeyEvent; import java.aw…

Rust图形界面:eGUI的Panel布局

文章目录 Panel布局尺寸调节源码 Panel布局 eGUI提供面板堆叠的布局方案&#xff0c;即Panel布局。其布局逻辑是&#xff0c;根据当前面板指定的方向&#xff0c;尽可能地填充空间。 CentralPanel 占据屏幕剩余部分的空间SidePanel 占据屏幕两侧的空间&#xff0c;在具体调用…

听GPT 讲Rust源代码--library/core/src(5)

题目来自 Understanding Box in Rust &#x1f980; File: rust/library/core/src/num/saturating.rs 在Rust的核心库中&#xff0c;源代码路径rust/library/core/src/num/saturating.rs所对应的文件是用来实现饱和运算的功能。 饱和运算是一种数值运算的方式&#xff0c;用于处…

中级程序员——uniapp和小程序面试题

&#x1f604;博主&#xff1a;小猫娃来啦 &#x1f604;文章核心&#xff1a;uniapp和小程序面试题 文章目录 用uniapp有遇到一些兼容性问题吗&#xff1f;uniapp最大的优点是什么&#xff1f;uniapp如何实现多端兼容&#xff1f;uniapp是如何做跨端适配的&#xff1f;常用的u…

1~2亿条数据需要缓存之安装redis集群(哈希取余分区、一致性哈希算法分区、哈希槽分区)

安装redis集群 面试题 1~2亿条数据需要缓存&#xff0c;请问如何设计这个存储案例??? 回答: 单机单台100%不可能&#xff0c;肯定是分布式存储&#xff0c;用redis如何落地&#xff1f; 上述问题阿里P6~P7工程案例和场景设计类必考题目&#xff0c; 一般业界有3种解决方案 …

Linux学习教程(第三章 Linux文件和目录管理)2

第三章 Linux文件和目录管理(初识Linux命令) 十一、Linux 删除空目录(rmdir命令) Linux rmdir命令:删除空目录 和 mkdir 命令(创建空目录)恰好相反,rmdir(remove empty directories 的缩写)命令用于删除空目录,此命令的基本格式为: [root@localhost ~]# rmdir […

走进ERP管理系统:企业信息化的加速器

走进ERP管理系统&#xff1a;企业信息化的加速器 企业信息化已成为企业提高效率、降低成本、优化资源配置的重要手段。ERP管理系统作为企业信息化的重要组成部分&#xff0c;已经成为了越来越多企业的必备工具。本文将带您走进ERP管理系统&#xff0c;探讨其优势、应用场景以及…

Redis篇---第三篇

系列文章目录 文章目录 系列文章目录前言一、为什么 Redis 需要把所有数据放到内存中?二、Redis 的同步机制了解是什么?三、pipeline 有什么好处,为什么要用 pipeline?前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到…

使用html2canvas插件进行页面截屏

使用纯html实现过程 <!DOCTYPE html> <html><head><title>使用html2canvas生成网页截图</title><script src"https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script> </head><body><h1>…

从Prefetch到Stream:重构v1.0代码库中的流式请求问题与解决方案

问题背景 在进行v1.0代码库的重构时&#xff0c;我们发现当前的prefetch参数存在一些问题。因此&#xff0c;我们计划将prefetch参数替换为stream。同时&#xff0c;我们决定在所有上传的操作中使用流式传输。 然而&#xff0c;是否需要对所有上传操作都进行流式传输存在一些疑…

设置chunk自动扩展到多大

1. 设置chunk自动扩展 execute function task(modify chunk extendable on,8); 2. 设置dbs扩展到多大合适 execute function task(modify space sp sizes,testdb1024,1024,10240) testdb 初始1MB 下次扩1MB 最大10MB

「Java开发指南」如何在Spring中使用JAX-WS注释器?

本文将指导您如何使用JAX-WS注释器从Spring服务生成JAX-WS Web服务&#xff0c;在本教程中&#xff0c;您将学习如何&#xff1a; 为Spring服务启用JAX-WS部署应用程序并测试服务 所有与Spring scaffolding相关的任务都需要MyEclipse Spring或Bling授权。 MyEclipse v2023.1…

成都瀚网科技有限公司抖音带货是否靠谱

成都瀚网科技有限公司&#xff0c;作为一家专业的科技公司&#xff0c;近年来积极参与了抖音带货的浪潮&#xff0c;凭借其出色的产品和服务&#xff0c;赢得了广大用户的信赖。 在当今的数字化时代&#xff0c;抖音带货已经成为了一种非常流行的购物方式。消费者可以通过抖音平…

elementui 实现树形控件单选

实现&#xff1a; <!--author: itmacydesc: 树节点单选 --> <template><div class"about"><el-tree :data"data"ref"tree":props"defaultProps"node-key"id"show-checkboxcheck-strictlycheck-change…

解决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…

Python中表格插件Tabulate的用法

目录 一、引言 二、Tabulate插件安装与导入 三、Tabulate基本用法 1、创建表格&#xff1a; 2. 格式化表格&#xff1a; 3. 表格转置&#xff1a; 4、合并单元格&#xff1a; 5、指定每列的格式&#xff1a; 6、指定每行的格式&#xff1a; 7、使用自定义表格格式&am…

kubernetes学习笔记-概念

参考&#xff1a;https://kubernetes.io/zh-cn/docs/concepts/overview/ 概述 Kubernetes 是一个可移植、可扩展的开源平台&#xff0c;用于管理容器化的工作负载和服务&#xff0c;可促进声明式配置和自动化。 Kubernetes 拥有一个庞大且快速增长的生态&#xff0c;其服务、…

观测云产品更新 | 监控、图表、服务管理、单点登录、Pipeline 等优化

观测云更新 监控相关的若干使用优化 1.【告警沉默】功能回归&#xff0c;您仍可以通过告警沉默配置定义重复告警通知的时间间隔。 2.【监控器】删除 ”紧急“、”重要“、”警告“触发条件必须配置任意一项的限制&#xff0c;您可任意配置”紧急“、”重要“、”警告“、”无…

实时云渲染与带宽探讨:解密数字新基建中的网络技术

随着“数字新基建”和5G技术的蓬勃发展&#xff0c;数字孪生、智慧工厂、智慧城市、虚拟仿真教学等领域正经历着迅速的技术进步。部署方案也由最初的本地部署&#xff0c;发展到webGL本地网络方式再到最新的实时云渲染技术。本文将深入探讨实时云渲染技术方案对于带宽的要求条件…