ThreadLocal是什么?
thread是线程,local是本地的意思
字面意思是线程本地。
其实更通俗的理解是给每个线程设置一个缓存。这个缓存用来存储当前线程在未来的业务逻辑中需要执行到的变量。
我们先来看怎么用:

首先创建全局变量ThreadLocal,
各自启动一个线程任务:
线程任务将变量设置到缓存中。
线程任务需要用到缓存中的变量时,直接从缓存中取即可。

 1 import java.util.concurrent.TimeUnit;
2
3 /**
4 * @discription
5 */
6 public class ThreadLocalLearn {
7 static ThreadLocal<String> threadLocal = new ThreadLocal<>();
8
9 public static void main(String[] args) {
10 Runnable r = new Runnable() {
11 @Override
12 public void run() {
13 threadLocal.set(Thread.currentThread().getName());
14 sayMyName();
15 threadLocal.remove();
16 }
17
18 public void sayMyName() {
19 for (int i = 0; i < 3; i++) {
20 String name = threadLocal.get();
21 System.out.println(Thread.currentThread().getName() + " say: im a thread, name:" + name);
22 try {
23 TimeUnit.SECONDS.sleep(3);
24 } catch (Exception e) {
25 //...
26 }
27 }
28 }
29 };
30 Thread t1 = new Thread(r);
31 t1.start();
32 Thread t2 = new Thread(r);
33 t2.start();
34 }
35 }

它的使用非常简单,
(1)先set()存储值;
(2)使用时get()取出值;
(3)用完了使用remove()清理掉;

输出如下:

Connected to the target VM, address: '127.0.0.1:56863', transport: 'socket'
Thread-0 say: im a thread, name:Thread-0
Thread-1 say: im a thread, name:Thread-1
Thread-0 say: im a thread, name:Thread-0
Thread-1 say: im a thread, name:Thread-1
Thread-1 say: im a thread, name:Thread-1
Thread-0 say: im a thread, name:Thread-0
Disconnected from the target VM, address: '127.0.0.1:56863', transport: 'socket'

很多人第一次见到ThreadLocal,第一直觉它的实现是用Map<Thread,Object> 。(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )但是深入研究之后,你会发现threadLocal的实现要比这样一个map 精妙的多,也好用的多。
我们通过查看java源码,可以依次探索ThreadLocal是如何实现缓存的:

类整体的关系大概是这样的:

查看源码,我们可以发现如下特性:

1、ThreadLocal本身并不是缓存,它只是起到一个缓存的key 的作用。我们每次创建一个ThreadLocal 并不是真正的创建了一个缓存,其实只是创建了一个缓存的标识。
源码如下:this 就是ThreadLocal实例

1     public void set(T value) {
2 Thread t = Thread.currentThread();
3 ThreadLocalMap map = getMap(t);
4 if (map != null) {
5 map.set(this, value);
6 } else {
7 createMap(t, value);
8 }
9 }

2、真正的缓存保存在Thread中,缓存被定义为:
ThreadLocal.ThreadLocalMap threadLocals;
从名字可以发现,这个缓存的类型是在ThreadLocal 中定义的一个静态内部类。这个类就是用来真正存放缓存的地方。这就像是thread小书包一样,每个线程有一个自己的独立的存储空间。
设计疑问:它(ThreadLocalMap)为什么没有定义在Thread类中,毕竟它是Thread的缓存。

源码如下:Thread.java

1     /* ThreadLocal values pertaining to this thread. This map is maintained
2 * by the ThreadLocal class. */
3 ThreadLocal.ThreadLocalMap threadLocals = null;

3、查看ThreadLocalMap的源码,我们发现它并没有实现Map接口,就像其他map一样,ThreadLocalMap实现了常用的Map中的set,get,getEntry,setThreshold,,remove 等方法。

并且它内部使用了线性探测法来解决哈希冲突。
设计疑问:它(ThreadLocalMap)为什么没有实现Map接口?
源码如下:ThreadLocal.Java

 1 static class ThreadLocalMap {
2
3 //...
4
5 private static final int INITIAL_CAPACITY = 16;
6
7
8 private Entry[] table;
9
10
11 private int size = 0;
12
13
14 private int threshold; // Default to 0
15
16
17 private void setThreshold(int len) {
18 threshold = len * 2 / 3;
19 }
20
21
22 private Entry getEntry(ThreadLocal<?> key) {
23 ...
24 }
25
26
27
28 private void set(ThreadLocal<?> key, Object value) {
29 ...
30 }
31
32
33 private void remove(ThreadLocal<?> key) {
34 ...
35 }
36
37
38 private void rehash() {
39 ...
40 }
41
42 private void resize() {
43 ...
44 }
45 ....
46 }

4、继续看源码,我们发现ThreadLocalMap类像其他Map实现一样,在内部定义了Entry。并且这个Entry居然继承了弱引用,弱引用被定义在Entry的key上,而且key的类型是ThreadLocal。

至于什么是弱引用,我以前的文章中介绍过,请看(浅谈Java中的引用   https://www.cnblogs.com/jilodream/p/6181762.html),一定要对弱引用了解,否则ThreadLocal的核心实现以及它会存在的问题,就无法更深理解了。

这里又会有疑问,为什么要使用弱引用,使用强引用不好吗?弱引用万一被回收导致空引用等问题怎么办?

源码如下:ThreadLocal.Java

1         static class Entry extends WeakReference<ThreadLocal<?>> {
2 /** The value associated with this ThreadLocal. */
3 Object value;
4
5 Entry(ThreadLocal<?> k, Object v) {
6 super(k);
7 value = v;
8 }
9 }

我们依次回答这几个问题:
(1)设计疑问:它(ThreadLocalMap)为什么没有定义在Thread类中,毕竟它是Thread的缓存
这恰恰是Thread符合开闭原则的优秀设计。如果是将ThreadLocalMap添加到Thread中,那么Thread类就太重了,以后只要和线程相关的业务都要将代码添加到Thread中,那Thread就无限膨胀了,变成超级类了,试想什么业务和线程能脱离关系呢?
况且他们只是类依赖关系而不是组合关系(对类关系不了解的同学可以看我的这篇文章:统一建模语言UML---类图  https://www.cnblogs.com/jilodream/p/16693511.html)。

Map怎么实现,缓存怎么维护,这些都是Thread不需要考虑的,我们就是需要用到你的特性。

(2)设计疑问:它(ThreadLocalMap)为什么没有实现Map接口?
实现接口是为了统一化提供接口,让外界可以只依赖接口,而不是接口的实现。但是ThreadLocalMap并不是给外界使用的,并不需要暴露出来。他就是为了给ThreadLocal业务使用的。只要完成最核心的Map能力,用空间换时间,将理论时间复杂度推向O(1)即可。因此完全没有必要实现Map接口。实现了Map接口反而要将内部方法暴露为public,这也不符合最少知道原则。一句话就是没必要,还添乱。

(3)为什么要使用弱引用,使用强引用不好吗?弱引用万一被回收导致空引用等问题怎么办?
我们需要先了解弱引用的特性:当一个变量只有弱引用关联时,那么在下次GC回收时,不论我们内存是否足够,都将回收掉该内存。
第一眼感觉这很危险,毕竟我们非常担心就是一个变量用着用着突然不能用了,出现空引用了,漫天的空引用这太不可控了。
其实这完全多虑了,注意看:我们是如何使用缓存的,是通过threadlocal.get(),也就是说我们想要使用缓存就一定要使用threadlocal的实例,也就是强引用,
有了强引用,使用时就一定不会被回收。因此完全不用担心使用缓存中,弱引用key突然变为null的情况了。
那什么时候弱引用key会被回收呢?
这就是当外界的强引用被手动设置为null时,(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )或者是作为局部变量跳出了方法栈,超出生命周期被回收掉了。
试想一下,真要是发生这两种情况,那么其实这个缓存也就根本无法再用到了同时,key被尽快回收,反而对内存更有利。
那么弱引用这么好用,为什么value不设置为弱引用呢?
其实细想一下就会发现value一定不能设置为弱引用,为什么呢?
key设置为弱引用,是因为想要使用这个缓存,key就一定要有强引用关联。而value则不一定有外界强引用关联,它在外界的强引用可能早就消失了。比如下面这个例子:

 1 import java.util.concurrent.TimeUnit;
2
3 /**
4 * @discription
5 */
6 public class ThreadLocalLearn {
7 static ThreadLocal<UserInfo> userContext = new ThreadLocal<>();
8
9 public static void main(String[] args) {
10 Runnable r = new Runnable() {
11 @Override
12 public void run() {
13 setUserInfo();
14 handle();
15 userContext.remove();
16 }
17
18 public void handle() {
19 UserInfo user = userContext.get();
20 //注意倘若map中的value被定义为弱引用,则此处的user可能为null
21 System.out.println(" i am:" + user.toString());
22 //do sth
23 try {
24 TimeUnit.SECONDS.sleep(3);
25 } catch (Exception e) {
26 //...
27 }
28 }
29 };
30 Thread t1 = new Thread(r);
31 t1.start();
32 Thread t2 = new Thread(r);
33 t2.start();
34 }
35
36 private static void setUserInfo() {
37 UserInfo user = new UserInfo();// 假装是从db中获取的
38 userContext.set(user);
39 //跳出该方法后,userInfo的在外部的直接强引用就被回收了
40 }
41 }
42
43 class UserInfo {
44 private String name;
45 private int age;
46
47 //....
48 }

我们在A方法中设置了缓存 currentUserId,跳出A方法,currentUserId在外界的引用被断开,倘若此时value也被定义为弱引用,value就随时可能被回收。而我们又可以通过

(key)Threadlocal  -->  threadLocals(ThreadLocalMap)  -->  entry  -->  value

这样的调用关系来拿到缓存value。这样缓存的使用就不可控了。
那么value一定不能设置为弱引用或及时回收么?
并不是,
其实我们只要在key回收时,顺手对value也做一个回收,但是这是GC完成的,再key消失时,联动对所有线程中关联的Map都进行一遍清理。(实现过于复杂)
亦或者清理key(threadlocal)的强引用时,将value的强引用也一并被清理。
可行,也是ThreadLocal推荐的方式,需要手动调用ThreadLocal.remove 方法。
在调用remove方法后,ThreadLocalMap会对所有垃圾数据进行清理,还会压缩哈希表。
为了解决ThreadLocalMap的value 延迟清理的情况,ThreadLocalMap在set get remove等方法中,都会对ThreadLocalMap存在的这种<null,Object> 垃圾数据进行一定程度的清理(注意这里要分各种情况,具体只能详细分析源码了,一篇博文很难说清)。

(4)这样又会有一个新的问题,如果key 被回收了,但是value没有被回收,因此value就常驻内存了,那么value不就会导致内存泄露吗?
很不幸,这样的确是会导致内存的泄露。(这里简单提一下,java中的内存泄露是指,可以通过强引用关联到他,gc无法回收掉它。与此同时,业务按照正常逻辑又无法使用到它。也就是又用不到,又回收不掉,就称之为内存泄露)
但是这种内存泄露出现的概率非常低。

它需要同时满足以下三个条件才可以:
1、需要线程的生命周期永远不会结束。如果线程生命周期结束了,那么ThreadLocalMap就会被回收,里边出现的无其他关联的key value 也都会被回收。
这种一般是守护线程或者线程池(线程复用出现)

2、ThreadLocal在设置为null时,没有手动调动remove方法

3、线程中的ThreadLocalMap在后续使用中,没有再调用任何get set remove方法,也就是线程没再使用ThreadLocal

概率低,是不是代表不太需要关注,当然不是。
因为内存泄露不仅仅是减少了可用内存,还增加了GC负担,系统性能就会收到影响,这就说的远了。

其实ThreadLocal最大的问题,并不是泄露的问题,而是被滥用的问题,不规范使用的问题。很多人把ThreadLocal当成是线程的私有仓库,所有变量参数都往里边塞,
导致写代码和维护时,非常不方便,出现问题也给维护人员造成很大的困扰。

接下来我们简单说下ThreadLocal的使用(后边我会再写一篇,如何使用使用ThreadLocal,毕竟我们学习技术目的是能够驾驭它,而不仅仅是知其所以然):
我们一般是将上下文信息,或者当前需要频繁使用的,与实际业务直接关系不大的系统数据方便携带。放置到thread的小书包中。
(1)上下文信息
如我们在controller层,将用户的上下文信息传入,如traceId(方便链路追踪),如用户token,后续可能调用其他鉴权接口等
(2)解耦数据库连接等连接池信息,
比如Springboot运行事务时,我们每次getconnection(),就只使用ThreadLocal中贮存好的这个连接,整个方法使用的是同一个数据库连接。
以上场景不使用ThreadLocal可以吗?
也可以,他并不是一定要使用。但是你这样就要把很多的参数传来传去,暴露很多的问题。
甚至在很多第三方实现的框架中,他不支持你传这些参数,他就是要用通过ThreadLocal来回传值。

(3)为线程安全提供了方案,减少了锁竞争:
如果说锁是从资源竞争的角度,解决了数据安全的问题。
ThreadLocal则是在每个线程中,只保存(只隔离)出与自己当前业务相关的数据。
注意他只是保证了数据的独立性,并不是独立创建了一份副本,(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )所以如果使用全局数据放置到value中时,一样可能会有数据安全问题。(当然这也是不推荐的用法)
比如有一份UserCache的全局缓存,多线程使用时,
我可以在全局中对UserCache进行加锁处理,也可以每个线程独立引用自己的UserInfo,线程之间互不干扰。结构就像这个样子:

全局加锁:

线程各自引用:

不知讲到这里大家还有没有最初的直觉了,为啥不设计一个全局的  Map<Thread,Object>。这样不是更简单,也更好定位问题:

细想一下,就会发现这样并不好:
方案1,全局只有一个Map,value是当前线程的所有缓存数据。那么Object就是一个非常复杂的数据,每次对Object进行读取都要解析的特别复杂。
方案2,全局定义的很多个Map,每个map是一个业务的缓存,比如User,就有userMap,token就有tokenMap。先不论Map本来就会有竞争的问题,对于管理大量的Map就是一件头痛的事情。

当然还是要根据具体业务来看,不能一概而论,并不能说任何时候使用ThreadLocal更好,使用全局Map更弱

浅谈ThreadLocal----每个线程一个小书包的更多相关文章

  1. 浅谈SQL注入风险 - 一个Login拿下Server

    前两天,带着学生们学习了简单的ASP.NET MVC,通过ADO.NET方式连接数据库,实现增删改查. 可能有一部分学生提前预习过,在我写登录SQL的时候,他们鄙视我说:“老师你这SQL有注入,随便都 ...

  2. 浅谈SQL注入风险 - 一个Login拿下Server(转)

    前两天,带着学生们学习了简单的ASP.NET MVC,通过ADO.NET方式连接数据库,实现增删改查. 可能有一部分学生提前预习过,在我写登录SQL的时候,他们鄙视我说:“老师你这SQL有注入,随便都 ...

  3. 浅谈 ThreadLocal

    有时,你希望将每个线程数据(如用户ID)与线程关联起来.尽管可以使用局部变量来完成此任务,但只能在本地变量存在时才这样做.也可以使用一个实例属性来保存这些数据,但是这样就必须处理线程同步问题.幸运的是 ...

  4. 转:浅谈SimpleDateFormat的线程安全问题

    转自:https://blog.csdn.net/weixin_38810239/article/details/79941964 在实际项目中,我们经常需要将日期在String和Date之间做转化, ...

  5. 浅谈ThreadLocal模式

    一.前言: ThreadLocal模式,严格意义上不是一种设计模式,而是java中解决多线程数据共享问题的一个方案.ThreadLocal类是java JDK中提供的一个类,用来解决线程安全问题,并不 ...

  6. 浅谈java中线程和操作系统线程

    在聊线程之前,我们先了解一下操作系统线程的发展历程,在最初的时候,操作系统没有进程线程一说,执行程序都是串行方式执行,就像一个队列一样,先执行完排在前面的,再去执行后面的程序,这样的话很多程序的响应就 ...

  7. 浅谈HTTP事务的一个过程

    一个腾讯在职的朋友问道,当我们在浏览器的地址栏输入 www.baidu.com ,然后回车,这一瞬间页面发生了什么?下面以谷歌浏览器一一解释. 一.域名解析 首先Chrome浏览器会解析www.bai ...

  8. struts 2学习笔记—浅谈struts的线程安全

    Sruts 2工作流程: Struts 1中所有的Action都只有一个实例,该Action实例会被反复使用.通过上面Struts 2 的工作流程的红色字体部分我们可以清楚看到Struts 2中每个A ...

  9. 浅谈HashMap与线程安全 (JDK1.8)

    HashMap是Java程序员使用频率最高的用于映射(键值对)处理的数据类型.HashMap 继承自 AbstractMap 是基于哈希表的 Map 接口的实现,以 Key-Value 的形式存在,即 ...

  10. 浅谈如何写出一个让(坑)人(王)很(之)难(王)发现的bug

    该文章内容来自脚本之家,原文链接:https://www.jb51.net/news/598404.html 程序员的日常三件事:写bug.改bug.背锅.连程序员都自我调侃道,为什么每天都在加班?因 ...

随机推荐

  1. jenkins部署github项目

    过程和gitee+jekins配置一样 方式1:使用ssh方式 jekins配置给github的认证信息 采取ssh方式,即需要提前将Jenkins本机的key添加到Gitlab上 先配置好ssh证书 ...

  2. aegis-dev-tools的使用

    前言aegis-dev-tools是一个辅助开发前端的工具包,用于根据后端接口文档生成前端封装api请求. 创建后端项目要求使用swagger,且controler书写规范,输入输出类型,备注都要 合 ...

  3. iPaaS中API接口管理平台的作用

    在iPaaS中,API接口管理平台具有关键作用,主要起到集成和连接不同系统之间的API,支持API文档和元数据管理,从代码注解扫描生成API.Swagger导入API.API自动识别.手工注册等多种方 ...

  4. SciTech-EECS-BigDataAIML-NN(神经网络): 常用的18种Activation(激活函数)

    SciTech-EECS-BigDataAIML-NN(神经网络): 常用的18种Activation(激活函数) 简介 为什么要用激活函数 激活函数的分类 常见的几种激活函数 4.0.Softmax ...

  5. Advanced Algebra高等代数 - 多元建模有多个方程(多元线性)组成 - 使用 NumPy 实现 矩阵的初等行变换:

    线性:指多元变量的每一元变量都是1次方(可以将高于1次方的元,以新一元变量代换,求解再做开方运算) 将应用问题转化为 多个多元线性方程,并成一组: 由多元线性方程组 抽出 增广矩阵,并以"消 ...

  6. SciTech-Mathematics-Probability+Statistics-Comparison:Chance + Possibility + Likelihood + Probability

    https://www.geeksforgeeks.org/what-is-the-difference-between-likelihood-and-probability/ 1. Chance 2 ...

  7. AI大模型时代已来,推荐几款主流的智能化低代码平台(附真实测评)

    ​ 随着AI大模型技术深入应用于企业管理应用的各方面,目前作为企业数字化应用构建的有力工具--低代码平台,也正迎来AI智能化升级.现在,市场上的各种低代码平台通过集成AI技术.自动化流程与数据分析等能 ...

  8. PostgreSQL 分区最佳实践

    概述 分区的本质是将一张大的物理表从逻辑上拆分,为 N 个较小的物理表. 分区表按照官方的解释如下: The partitioned table itself is a "virtual&q ...

  9. 如何基于 ZEGO SDK 实现回声消除、自动增益控制、降噪功能

    1 概述 在实时音视频通话时,可以对音频进行 3A 处理,提高通话质量和用户体验.主要包括 AEC(Acoustic Echo Cancelling,回声消除),AGC(Automatic Gain ...

  10. 优雅管理模态框:基于 Pinia 的类型安全的解决方案

    引言 在现代前端应用中,模态框(Modal)是最常用的UI组件之一.然而,随着应用规模的增长,模态框的状态管理往往会变得混乱.本文将介绍一种基于 Pinia 和 Vue 3 Composition A ...