什么是ThreadLocal?

ThreadLocal主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程是相对隔离的,在多线程的情况下,防止自己的数据被别的线程修改。

ThreadLocal在面试中也有很多面试官喜欢问,很多人认为数据是存在ThreadLocal中的,key是当前线程。

首先这样是错误的,实际上java中线程共享机制,最重要的是Thread中的ThreadLocalMap,ThreadLocal并不重要,它只是一个”钩子“,数据实际上被存在ThreadLocalMap中,下面慢慢道来:

ThreadLocal的使用

public class Test01 {public static void main(String[] args) {ThreadLocal<String> threadLocal = new ThreadLocal<>();threadLocal.set("lisi");String s = threadLocal.get();System.out.println(s);new Thread(new Runnable() {@Overridepublic void run() {String s1 = threadLocal.get();System.out.println(s1);}}).start();}
}
结果:
lisi
null

ThreadLocal.set(T value)的key是Thread吗?

首先,先告诉大家,ThreadLocal并不保存数据,ThreadLocalMap的key也不是Thread。

那为什么会有人会认为key是当前线程呢?看看set()的源码


createMap(t, value),传入了一个Thread和value,很多人就会认为ThreadLocal内部有一个Map,key就是Thread了,但事实是这样的吗?

并不是,看逻辑,先不管map是哪里来的,if(map !=null) map.set(this,value);也就是把this作为了map的key,那么这个this是谁呢,很显然是ThrealLocal。

再来说说map是怎么来的?

答案就在createMap(t,value)中,t是当前线程:


原来是通过create()方法new了一个ThreadLocalMap,并做了初始化,把当前ThreadLocal对象作为了key,firstValue作为了value。

到这里我们至少知道了两点:

  1. 有一个map,来历还不知道,数据存在这里,key是当前ThreadLocal对象,而不是当前线程
  2. Map类型是ThreadLocalMap,被赋值给Thread中的一个字段threadLocals

接着来:

如何理解ThreadLocal是一个钩子?


从上图可以看出,Thread t1 实例化后,内部会有一个ThreadLocalMap,刚开始为null,当执行到

ThreadLocal tl1 = new ThreadLocal();
t1.set("你好");

ThreadLocal会作为一个钩子,尝试从Thread T1中勾出ThreadLocalMap,如果发现这个成员变量尚未赋值,则new ThreadLocalMap()并把map设置进去。特别注意,由于set()是ThreadLocal的方法,所以map.set(this, value)中的this显然是ThreadLocal tl1。

此时此刻,内存中有三个对象,Thread t1、ThreadLocal tl1、ThreadLocalMap map,其中ThreadLocalMap在Thread内部,是ThreadLocal塞进去的。

ThreadLocalMap底层是什么样子的呢?

既然有个map,那他的数据结构应该和HashMap很像,但是从源码上看,它并未实现map接口,而且它的Entry是继承WeakReference(弱引用)的,也没有看到HashMap中的next,所以不存在链表了。

大概就是这样了:

为什么需要数组呢?没有了链表怎么解决Hash冲突呢?

用数组是因为,我们开发过程中可以一个线程可以有多个ThreadLocal来存放不同类型的对象,他们都将存放在当前线程的ThreadLocalMap中,需要用数组接收。

至于Hash冲突,我们先看一下源码:

private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();if (k == key) {e.value = value;return;}if (k == null) {replaceStaleEntry(key, value, i);return;}}tab[i] = new Entry(key, value);int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}

从源码可以看到,ThreadLocalMap在存储的时候会给每一个ThreadLocal对象一个threadLocalHashCode,在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,int i = key.threadLocalHashCode & (len-1)。

然后会判断,如果当前位置是空的,就初始化一个Entry对象放在位置i上;

if (k == null) {replaceStaleEntry(key, value, i);return;
}

如果不为空,如果这个Entry对象的key正好是即将设置的key,那么就刷新Entry中的value;

if (k == key) {e.value = value;return;
}

如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止。

这样的话,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置,set和get如果冲突严重的话,效率还是很低的。

以下是get的源码,是不是就感觉很好懂了:

private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)return e;elsereturn getEntryAfterMiss(key, i, e);}private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;int len = tab.length;
// get的时候一样是根据ThreadLocal获取到table的i值,然后查找数据拿到后会对比key是否相等  if (e != null && e.get() == key)。while (e != null) {ThreadLocal<?> k = e.get();// 相等就直接返回,不相等就继续查找,找到相等位置。if (k == key)return e;if (k == null)expungeStaleEntry(i);elsei = nextIndex(i, len);e = tab[i];}return null;}

对象存放在哪里?

在Java中,栈内存归属于每一个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属的线程可见,即栈内存是线程私有内存,而堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问。
那么是不是说ThreadLocal的实例以及数据存放在栈上呢?
其实不是的,因为ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有),而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。

共享线程的ThreadLocal数据

使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。


结果:

这又是怎么传递的呢?

Thread源码中,我们看看Thread.init初始化创建的时候做了什么:

public class Thread implements Runnable {……if (inheritThreadLocals && parent.inheritableThreadLocals != null)this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);……
}

如果线程的inheritThreadLocals变量不为空,比如我们上面的例子,而且父线程的inheritThreadLocals也存在,那么我就把父线程的inheritThreadLocals给当前线程的inheritThreadLocals。

多个Thread与同一个ThreadLocal

为什么访问同一个threadLocal.get(),Thread1存入的值不会被Thread2取出来?
上代码:

ThreadLocal zixia = new ThreadLocal();// 至尊宝
new Thread(()->{// 遇到紫霞,紫霞往他心里塞了眼泪zixia.set("泪水");
}).start();// 孙悟空
new Thread(()->{// 遇到紫霞,紫霞往他心里塞了紫青宝剑zixia.set("紫青宝剑");
}).start();

一个线程中有一个ThreadLocalMap,至尊宝这个线程中ThreadLocalMap保存了已zixia为key,”泪水“为value的entry;孙悟空也在自己的线程中保存了一个entry,两个线程里面的东西都不一样,又怎么会串起来呢?

同一个Thread与多个ThreadLocal

ThreadLocal zixia = new ThreadLocal();
zixia.set("泪水");ThreadLocal baijingjing = new ThreadLocal();
baijingjing.set("回忆");

一个Thread中有一个ThreadLocalMap,第一次遇到的ThreadLocal会帮他创建一个map塞进去,之后无论遇到多少个ThreadLocal,都是直接用那个map,并把自己作为key,存到map中。

多个Thread与多个ThreadLocal

ThreadLocalMap本质是每个Thread内部各存一份,互不干扰。Thread在遇到不同的ThreadLocal,可以把ThreadLocal自身作为key存入map或从map中取出value。

ThreadLocalMap和WeakReference(弱引用)

在java中有4中类型的引用:强、软、弱、虚。

  • 强引用不受GC的影响,除非引用全部切断,Student student = new Student();这就是强引用,当student=null,student对象会在下次GC时被回收。
  • 软引用对象会在内存不足触发GC时被回收(适用于高速缓存)
  • 弱引用是每次GC时都回收
  • 虚引用(堆外内存,比如zerocopy)

对于Map,每一个键值对被称为Entry,相信大家都知道。


为什么ThreadLocalMap的Entry要继承弱引用呢?

虽然Entry继承了WeakReference,但并不是说Entry本身是弱引用,而是Entry的key是弱引用:

于是问题由为什么ThreadLocalMap的Entry要继承弱引用转为ThreadLocalMap为什么要把key包装成弱引用。


如果ThreadLocalMap的key是强引用,那么只要线程存在,ThreadLocalMap就存在,而ThreadLocalMap是entry数组,对应的entry数组就存在,entry的key是ThreadLocal,即使我们在代码中显示的赋值threadlocal=null,告诉CG要回收该对象,由于上面的强引用存在,不会被回收的。

ThreadLocalMap与内存泄露

原本引入Weak Reference是为了解决多个强引用导致ThreadLocal对象无法回收的问题,但一个解决策略的引入往往伴随着新bug的产生。试想一下,当外部强引用都切断后下一次GC回收了ThreadLocal对象,此时Entry的key会变成什么?

当tl1变成null,ThreadLocalMap的Entrys变成下面这样:
● null : value1(Entry1)
● tl2 : value2(Entry2)
● tl3 : value3(Entry3)

这就导致了一个问题,ThreadLocalMap中的key是弱引用,在没有外部强引用时,下次GC会被回收,如果创建的ThreadLocal线程一直持续运行,那么这个entry数组中的value就有可能一直不会被回收,发生内存泄漏。

比如线程池中的线程,线程都是复用的,那么之前的线程实例处理完之后,处于复用的原因线程依然存活(线程复用就是将线程阻塞,线程中一直持有entry数组,导致不会被GC回收),最终导致内存泄漏。

怎么解决?

在代码的最后一行remove就好了:

ThreadLocal<String> localName = new ThreadLocal();
try {localName.set("张三");……
} finally {localName.remove();
}

实际上,ThreadLocalMap也发现了这个问题,它会在每次get/set时判断key,如果key为null,则把value也归置为null:

当这种策略有风险,因为它的前提是下一次使用时会把上一次的key为null的清除,如果在也不用,是不是仍然没有清除。

所以最保险的方法是,每次使用完毕都及时清除。
看一下remove方法:

《阿里巴巴开发手册》也是这样建议:

一般来说,用完立即移除是最好的,但实际编程时有些公司喜欢在拦截器中取出用户信息放入线程,对此个人建议可以在拦截器的preHandle()中set,在afterCompletion()中remove()。

封装ThreadLocal

为什么要封装ThreadLocal

原因有两点:

  1. 对于Thread,如果希望在Interceptor中存入UserInfo并在Service层通过ThreadLocal把UserInfo钩出来,必须保证Interceptor和Service此时用的是同一个ThreadLocal。

我们可以创建一个ThreadLocalUtil中new一个ThreadLocal对象作为成员变量,就可以在service中取出来了:

public class ThreadLocalUtil{private ThreadLocalUtil(){};private static final ThreadlLocal<Map<String,Object>> ZI_XIA  = new ThreadLocal();}


2. 原生的ThreadLocal无法满足复杂的业务场景
比如现在我封装了一个最简单的ThreadLocal(装饰者模式,为的是解决第一个问题):

package com.example.czy.util;public class MyThreadLocal {public MyThreadLocal(){};private static  final ThreadLocal<Object> THREAD_LOCAL= new ThreadLocal<>();public static void put(Object object){THREAD_LOCAL.set(object);}public static Object get(){return THREAD_LOCAL.get();}public static  void delete(){THREAD_LOCAL.remove();}
}

MyThreadLocal确实解决了第一个问题,复用了ThreadLocal,保证了Interceptor和Service用到的ThreadLocal是同一个对象。



但是,有两个缺陷:
● 无法存取多个不同的值
● 语意不明

比如,Service层希望往ThreadLocal里再添加一个Score对象,好让DAO层能获取到。你要怎么做?

ThreadLocalUtil

public class ThreadLocalUtil {public ThreadLocalUtil(){}/*** 存入的是一个map集合,这样做的目的是可以存入不同的值* 比如,你想存入user类型 ,map的结构就是* THREAD_LOCAL:{*        user:{name=..,age=..}*  }*  如果在向map里面put一个student,map的结构是这样的*  THREAD_LOCAL:{*           user:{name=..,age=..},*           student:{name=..,age=..}}*/private static final ThreadLocal<Map<String,Object>> THREAD_LOCAL = new ThreadLocal<>();/*** 存入线程变量*/public static  void put(String key , Object o){//从ThreadLocal取出数据Map<String, Object> map = THREAD_LOCAL.get();//判断是否为空if (map==null){map = new HashMap<>();//把map放入到ThreadLocal中THREAD_LOCAL.set(map);}//在map中存放数据map.put(key,o);}/*** 取出线程变量*/public static Object get(String key){Map<String, Object> map = THREAD_LOCAL.get();return map!=null?map.get(key):null;}/*** 移除当前线程指定的变量*/public static  void remove(String key){Map<String, Object> map = THREAD_LOCAL.get();map.remove(key);}/*** 移除当前线程全部的变量*/public static void clear(){THREAD_LOCAL.remove();}}

spring对ThreadLocal的封装

比如编写AOP日志时,经常会用到的RequestContextHolder,其实内部也维护了ThreadLocal。



那么Spring是如何做到remove的呢?使用过滤器(我们使用了拦截器)。

并发编程-基础篇五-ThreadLocal相关推荐

  1. java并发编程入门_探讨一下!Java并发编程基础篇一

    Java并发编程想必大家都不陌生,它是实现高并发/高流量的基础,今天我们就来一起学习这方面的内容. 什么是线程?什么是进程?他们之间有什么联系? 简单来说,进程就是程序的一次执行过程,它是系统进行资源 ...

  2. 并发编程基础篇——第一章(并发相关基础概念理解)

    其实讲到并发编程,有时候会问自己为什么要去做这些知识的积累和沉淀,可能我们做业务的在职业生涯里,并不会经常使用到这些所谓的多线程编程,顶多可能开一个线程,去执行个任务,又或者通过定时器触发某个业务,实 ...

  3. 泥瓦匠聊并发编程基础篇:线程中断和终止

    原文:www.spring4all.com 1 线程中断 1.1 什么是线程中断? 线程中断是线程的标志位属性.而不是真正终止线程,和线程的状态无关.线程中断过程表示一个运行中的线程,通过其他线程调用 ...

  4. 并发编程基础篇——第二章(如何创建线程)

    上节讲了基础概念,本章正式进入线程专题,对基础薄弱的同学可以好好看本章!! 1.Thread匿名子类 我们可以通过下面的代码来直接创建一个线程. // 构造方法的参数是给线程指定名字,推荐 Threa ...

  5. 《JUC并发编程 - 基础篇》JUC概述 | Lock接口 | 线程间通信 | 多线程锁 | 集合线程安全

  6. Java并发编程基础--ThreadLocal

    Java并发编程基础之ThreadLocal ​ ThreadLocal是一个线程变量,但本质上是一个以ThreadLocal对象为键.任意对象为值的存储结构,这个结构依附在线程上,线程可以根据一个T ...

  7. Java并发编程|第二篇:线程生命周期

    文章目录 系列文章 1.线程的状态 2.线程生命周期 3.状态测试代码 4.线程终止 4.1 线程执行完成 4.2 interrupt 5.线程复位 5.1interrupted 5.2抛出异常 6. ...

  8. Java 并发编程之美:并发编程高级篇之一-chat

    借用 Java 并发编程实践中的话:编写正确的程序并不容易,而编写正常的并发程序就更难了.相比于顺序执行的情况,多线程的线程安全问题是微妙而且出乎意料的,因为在没有进行适当同步的情况下多线程中各个操作 ...

  9. Java 并发编程之美:并发编程高级篇之一

    借用 Java 并发编程实践中的话:编写正确的程序并不容易,而编写正常的并发程序就更难了.相比于顺序执行的情况,多线程的线程安全问题是微妙而且出乎意料的,因为在没有进行适当同步的情况下多线程中各个操作 ...

最新文章

  1. 病毒周报(091102至091108)
  2. Material Design控件使用学习 TabLayout+SwipeRefreshlayout
  3. Android SimpleAdapter显示ListView、GridView
  4. Vs定义超大数组时,stack OverFlow的解决方法
  5. PMCAFF高端俱乐部首次集结,最顶级产品人的私密俱乐部!
  6. 二十四种设计模式:代理模式(Proxy Pattern)
  7. rxjs switchMap的实现原理
  8. dateframe行列插入和删除操作
  9. 使用vert.x 2.0,RxJava和mongoDB创建simpe RESTful服务
  10. smtplib 抄送邮件_用Python收发电子邮件
  11. 第67课 选择排序 例67.1 《小学生C++编程入门》
  12. BZOJ1370 [Baltic2003]Gang团伙
  13. 用keil5将程序下载到板子里
  14. u大师u盘装系统win7_黑鲨U盘重装win7系统教程
  15. padavan手动安装php
  16. 时间复杂度:1秒内能执行多少指令
  17. web前端是什么?需要掌握什么技术
  18. 小白学习MySQL - 聊聊数据备份的重要性
  19. 自定义高性能播放器, 实现边下边播缓存等功能
  20. 华栖云与阿里云首推“云上电视台”,可实现内容云端一站式制作

热门文章

  1. URL Schemes 的发展
  2. 使用SC 修改服务启动账户
  3. c语言实数的存放形式,C51中float定义的实数存放形式
  4. app后端 服务器端 后台 部署图
  5. 2018下半年Android面试历程
  6. 这心态也太好了!阿水赛前与kid双排狂说骚话:赢了血赚输了不亏
  7. 在Nuxt项目中使用iconfont阿里巴巴图标unicode
  8. 一个矩阵与单位矩阵相乘等于本身吗?并且符合交换律吗?
  9. 获取和设置默认打印机
  10. 提前和2022年6月的自己聊聊