文章目录
- ThreadLocal学习笔记
- 前言
- 1、TheadLocal基本介绍
- 2、ThreadLocal基本使用
- 3、体验ThreadLocal的优点
- 3.1 ThreadLocal与synchronized的比较
- 3.2、ThreadLoca的应用场景
- 4、ThreadLocal的内部原理
- 4.1 ThreadLocal内部结构
- 4.2 ThreadLocal常用方法分析
- 4.2.1 set原理分析
- 4.2.2 get原理分析
- 4.2.3 remove原理分析
- 4.3 ThreadLocalMap源码分析
- 4.3.1 ThreadLocal基本结构
- 5、ThreadLocal存在的问题
- 5.1 内存泄漏问题
- 5.2 Hash冲突问题
- 相关面试题
ThreadLocal学习笔记
前言
本文主要是对ThreadLocal的源码进行解析,从而在面对面试官提问ThreadLoca相关方面的问题时,更加有底气一点,因为很多面试题光死记硬背心中会有疑惑,同时记得不够劳,所以这里我选择花点时间阅读一下ThreadLocal的源码,从而对ThreadLocal有更加深刻的了解,从目录也可以看出本文的内容大致是讲什么(主要有
set
、get
、remove
三个常用方法的源码解析,以及ThreadLocal常见的问题内存泄露问题、Hash冲突问题、同时相关面试题也被我整理在另外一个20w字的.md文档,想要的可以评论区回复666即可领取),关于特别深层次的东西我暂且没有深究,毕竟我还是一个菜鸟小白,等以后工作之后再抽空慢慢研究
1、TheadLocal基本介绍
-
什么是ThreadLocal?
- Java官方文档中的描述:ThreadLocal类用来提供线程内部的局部变量,这些局部变量在多线程下访问(通过get\set方法访问)时,能够保障各个线程的变量相对独立与其它线程内的局部变量,ThreadLocal实例通常来说都是private state 类型的,用于关联线程和线程上下文
- 我的理解:ThreadLocal相当于是一个容器,每一个线程都有与之对应的ThreadLocal容器,就类似于Session一样,可以用来存储数据,同时每个容器都相互隔离、互不影响
-
ThreadLocal是用来干什么的?
- 提供线程内部的局部变量,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度
-
ThreadLocal的适用场景有哪些?
- 数据库连接管理:在多线程环境中,每个线程需要独立地获取数据库连接,使用 ThreadLocal 可以避免线程之间的连接共享和线程安全问题。
- 用户身份认证:在 Web 应用中,每个用户需要进行身份认证,使用 ThreadLocal 可以将用户身份信息存储在 ThreadLocal 中,确保用户身份信息在线程中独立存在,并且不会被其他线程访问到。(这个很常用)
- 事务管理:在多线程环境中,事务管理可能会存在线程安全问题,使用 ThreadLocal 可以将事务信息存储在 ThreadLocal 中,确保每个线程之间的事务互不干扰。
- 线程安全工具类:在某些情况下,我们需要使用线程安全的工具类,例如 SimpleDateFormat 和 Random 等,但这些工具类本身并不是线程安全的。使用 ThreadLocal 可以将这些非线程安全的工具类变为线程安全的。
- 线程上下文信息传递:在多线程环境中,有时需要将某些信息从一个线程传递到另一个线程,使用 ThreadLocal 可以将信息存储在 ThreadLocal 中,然后在另一个线程中获取这些信息。
Spring的事务隔离就是使用ThreadLocal+AOP来解决的;线程池+ThreadLocal解决SimpleDateFormat线程安全问题
-
如何使用ThreadLocal?
主要是调用ThreadLocal的get和set方法,set方法将数据存储到ThreadLocal中,然后get获取当前线程绑定的ThreadLocal中对应的数据
知识拓展:线程上下文
线程上下文(Thread Context)是指在一个线程中执行代码时,该线程所拥有的所有上下文信息,包括线程的状态、线程的优先级、线程的堆栈信息、线程的局部变量、线程的环境变量等。线程上下文可以影响线程的行为和执行结果。
- 线程上下文的分类:
- 线程状态上下文:包括线程的状态(如 NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING 和 TERMINATED 等)和优先级。
- 线程堆栈上下文:包括线程的堆栈信息,如栈帧、方法调用链等。
- 线程局部变量上下文:包括线程的局部变量、参数等信息。
- 线程环境变量上下文:包括线程的环境变量、系统属性等信息。
- 线程上下文的作用:
- 传递参数:可以在一个线程中的方法之间传递参数,而不需要将参数显式地传递给每个方法。
- 状态保存:可以保存线程的状态和执行上下文,以便在需要时恢复线程的执行状态。
- 线程隔离:可以将线程的数据隔离开来,避免多个线程之间的数据共享和竞争。
- 环境隔离:可以将线程的环境变量和系统属性隔离开来,避免对全局环境造成影响。
- 调试支持:可以在调试时查看线程的状态和执行上下文,帮助定位问题。
2、ThreadLocal基本使用
-
ThreadLocal中的常用API
API 描述 ThreadLocal()
ThreadLocal函数的构造方法,用于创建ThreadLocal对象 public void set(T value)
设置当前线程中绑定的局部变量 public T get()
获取当前线程中绑定的局部变量 public void remove()
移除当前线程中绑定的局部变量
实例一:
不使用ThreadLocal,在多线程情况下进行存取变量
public class MyDemo01 {private String content;private String getContent(){return content;}private void setContent(String content){this.content = content;}public static void main(String[] args) {MyDemo01 demo = new MyDemo01();demo.setContent(Thread.currentThread().getName()+"的数据");System.out.println(Thread.currentThread().getName() + "===>" + demo.getContent());for (int i = 0; i < 5; i++) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {// 存数据demo.setContent(Thread.currentThread().getName() + "的数据");try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}// 取数据System.out.println(Thread.currentThread().getName()+ "===>" + demo.getContent());}});thread.setName("线程"+i);thread.start();}}
}
结果展示:
可以发现多线程先,一个线程存数据,一个线程取数据,同一个线程并不能百分百拿到它原本存进去的数据
备注:由于线程存在随机性,当所有线程都拿到自己存的数据,纯属巧合,可以多试几次才能出现不同的情况
上面代码的运行图如下所示(可能有点抽象):
实例二:
使用ThreadLocal在多线程下存取变量
public class MyDemo02 {private ThreadLocal<String> threadLocal = new ThreadLocal<>();private String content;private String getContent() {return threadLocal.get();}private void setContent(String content) {threadLocal.set(content);}public static void main(String[] args) {MyDemo02 demo = new MyDemo02();demo.setContent(Thread.currentThread().getName()+"的数据");System.out.println(Thread.currentThread().getName() + "===>" + demo.getContent());for (int i = 0; i < 5; i++) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {// 存数据demo.setContent(Thread.currentThread().getName() + "的数据");try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}// 取数据System.out.println(Thread.currentThread().getName() + "===>" + demo.getContent());}});thread.setName("线程" + i);thread.start();}}
}
上面代码运行示意图如下所示:
3、体验ThreadLocal的优点
3.1 ThreadLocal与synchronized的比较
- 作用不同:synchronized主要是针对对象或者类的,主要是防止多个线程同时执行一个代码块产生的问题;ThreadLocal主要针对变量的,主要是防止共享变量在多线程下产生的安全问题
- 机制不同:synchronized 是采用锁(互斥锁)的机制,保证一次只有一个线程执行代码,在前一个线程执行代码期间其他线程都处于等待状态
synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制采用“以时间换空间”的方式,只提供了一份变量,让不同的线程排队访问 | 采用了“以空间换时间”的方式,为每一个线程提供一份变量的副本,从而实现同时访问互不干扰 |
侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据互相隔离 |
示例
示例一:
不使用ThreadLocal,在多线程下存取变量
/*** @author ghp* @date 2023/2/11* @title 不使用ThreadLocal存取变量* @description*/
public class MyDemo01 {private String content;private String getContent() {return content;}private void setContent(String content) {this.content = content;}public static void main(String[] args) {MyDemo01 demo = new MyDemo01();demo.setContent(Thread.currentThread().getName() + "的数据");System.out.println(Thread.currentThread().getName() + "===>" + demo.getContent());for (int i = 0; i < 5; i++) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {// 存数据demo.setContent(Thread.currentThread().getName() + "的数据");try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}// 取数据System.out.println(Thread.currentThread().getName() + "===>" + demo.getContent());}});thread.setName("线程" + i);thread.start();}}
}
示例二:
使用ThreadLocal,在多线程下存取变量
/*** @author ghp* @date 2023/2/12* @title 使用ThreadLocal存取变量* @description*/
public class MyDemo02 {private ThreadLocal<String> threadLocal = new ThreadLocal<>();private String content;private String getContent() {return threadLocal.get();}private void setContent(String content) {threadLocal.set(content);}public static void main(String[] args) {MyDemo02 demo = new MyDemo02();demo.setContent(Thread.currentThread().getName()+"的数据");System.out.println(Thread.currentThread().getName() + "===>" + demo.getContent());for (int i = 0; i < 5; i++) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {// 存数据demo.setContent(Thread.currentThread().getName() + "的数据");try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}// 取数据System.out.println(Thread.currentThread().getName() + "===>" + demo.getContent());}});thread.setName("线程" + i);thread.start();}}
}
示例三:
使用 synchronized 在多线程下存取变量
/*** @author ghp* @date 2023/2/12* @title 使用synchronized锁* @description*/
public class MyDemo03 {private String content;private String getContent() {return this.content;}private void setContent(String content) {this.content = content;}public static void main(String[] args) {MyDemo03 demo = new MyDemo03();demo.setContent(Thread.currentThread().getName()+"的数据");System.out.println(Thread.currentThread().getName() + "===>" + demo.getContent());for (int i = 0; i < 5; i++) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {synchronized (MyDemo03.class){// 存数据demo.setContent(Thread.currentThread().getName() + "的数据");try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}// 取数据System.out.println(Thread.currentThread().getName() + "===>" + demo.getContent());}}});thread.setName("线程" + i);thread.start();}}
}
3.2、ThreadLoca的应用场景
本小节演示使用ThreadLocal解决单一数据库连接,SimpleDateFormat线程不安全问题,多参数传递问题
示例
示例一:使用ThreadLocal保障Service和Dao层的连接对象一致
场景:在进行购买书籍时,我们需要先查询库存,判断库存是否充足,库存充足则用户余额扣除,然后书的数量-1。
这一过程涉及到两个两张表的操作,tb_user 和 tb_book,在进行两张表的SQL操作时,我们需要引入事务,但是引入事务,想要事务生效我们需要保障:
- service层和DAO层的连接对象保持一致
- 每个线程的连接对象前后需保持一致
常见的解决方案:
-
加锁:将数据库连接对象锁住
优点:实现简单;缺点:性能较低
-
注入:将service层的连接对象直接注入到dao层(这个方法很常见)
优点:实现简单;缺点:提高了代码的耦合性,降低了程序的性能
-
线程隔离连接对象:使用ThreadLocal存储连接对象
数据传递:保证每一个线程绑定的数据,在需要的地方直接获取,减少了代码的耦合性
线程隔离:各个线程之间的数据相互隔离,又具备并发性,避免同步带来的性能损耗
示例二:使用ThreadLocal解决SimpleDateFormat线程安全
场景:在多线程下,调用SimpleDateFormat的prase方法对字符串进行日期格式化(这个场景很常见)
import java.text.ParseException;
import java.text.SimpleDateFormat;/*** @author ghp* @date 2023/4/12* @title* @description 测试SimpleDateFormat是否线程安全*/
public class MyDemo04 {private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");public static void main(String[] args) {// 创建10个线程,每个线程都调用SimpleDateFormat对字符串进行日期格式化for (int i = 0; i < 10; i++) {new Thread(()->{dateFormat();}).start();}}/*** 将字符串解析为日期*/private static void dateFormat() {try {String date = simpleDateFormat.parse("2023-4-5").toString();System.out.println("date = " + date);} catch (ParseException e) {throw new RuntimeException(e);}}
}
直接报错:
-
报错原因
当我们使用
SimpleDateFormat
的parse()
方法时,parse()
方法会先调用calendar.clear()
方法,然后调用Calendar.add()
方法,如果一个线程先调用了add()
方法,然后另一个线程调用了clear()
方法,这个时候parse()
就会报错。所以说==SimpleDateFormat
是线程不安全的== -
解决方法:使用ThreadLocal+线程池
import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;/*** @author ghp* @date 2023/4/12* @title* @description 使用 ThreadLocal 解决 SimpleDateFormat 线程不安全问题*/ public class MyDemo05 {private static ThreadLocal<SimpleDateFormat> threadLocal =new ThreadLocal<SimpleDateFormat>(){@Overrideprotected SimpleDateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd");}};private static final ExecutorService executorService = Executors.newFixedThreadPool(10);public static void main(String[] args) {// 调用线程池,每个线程都调用SimpleDateFormat对字符串进行日期格式化for (int i = 0; i < 10; i++) {executorService.execute(()->{dateFormat();});}// 关闭线程池executorService.shutdown();}/*** 将字符串解析为日期*/private static void dateFormat() {try {String date = threadLocal.get().parse("2023-4-5").toString();System.out.println("date = " + date);} catch (ParseException e) {throw new RuntimeException(e);}}}
这里还提供另一种方法,直接不用SimpleDateFormat,改用 Java8 提供的
DateTimeFormatter
进行日期的格式化,如下所示:import java.time.LocalDateTime; import java.time.format.DateTimeFormatter;/*** @author ghp* @date 2023/4/12* @title* @description 测试 Java8 提供的 DateTimeFormatter 是否线程安全*/ public class MyDemo06 {private static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");public static void main(String[] args) {final int threadCount = 1000;// 创建10个线程,每个线程都调用 LocalDateTime 对字符串进行日期格式化for (int i = 0; i < threadCount; i++) {new Thread(()->{dateFormat();}).start();}}/*** 将字符串解析为日期*/private static void dateFormat() {String date = LocalDateTime.parse("2020-03-30 08:08:08", dateTimeFormatter).toString();System.out.println("date = " + date);}}
==注意:==LocalDateTime直接解析 yyyy-MM-dd 格式的日期,会直接报错,具体解决方法这里不作过多解释,详情请参考这篇文章:关于yyyy-MM-dd格式日期字符串,解析成LocalDateTime遇到的问题
示例三:使用ThreadLocal解决过度传参
场景:一个方法要调用好多个方法,并且每一个方法都要传递参数
不使用ThreadLocal:
/*** @author ghp* @date 2023/4/12* @title* @description 使用ThreadLocal解决过度传参*/
public class MyDemo07 {void A(User user){getInfo(user);checkInfo(user);log(user);....}private void log(User user) {...}private void checkInfo(User user) {...}private void getInfo(User user) {...}
}
使用ThreadLocal:
/*** @author ghp* @date 2023/4/12* @title* @description 使用ThreadLocal解决过度传参*/
public class MyDemo07 {private static ThreadLocal<User> threadLocal = new ThreadLocal<>();void A(User user){threadLocal.set(user);getInfo();checkInfo();log();....}private void log() {User user = threadLocal.get();...}private void checkInfo() {User user = threadLocal.get();...}private void getInfo() {User user = threadLocal.get();...}
}
使用ThreadLocal后,能够有效降低层与层之间的耦合,比如我们在登录时,我们可以直接将登录的用户信息存入ThreadLocal中,这样其它层要使用ThreadLocal,可以直接通过ThreadLocal获取,一来降低了层与层之间的代码耦合,二来也保障了线程安全
4、ThreadLocal的内部原理
4.1 ThreadLocal内部结构
-
早期的ThreadLocal(JDK1.8以前):每一个ThreadLocal都创建一个Map,然后使用Thread作为Map的key,要存储的局部变量作为Map的value
-
JDK1.8中的ThreadLocal:每一个ThreadLocal都维护一个ThreadLocalMap,然后使用ThreadLocal的实例对象作为Map的key,以一个Object对象作为Map的value。具体过程如下所示:
1)每一个Thread线程内部都有一个Map(ThreadLocalMap)
2)Map里面存储的ThreadLocal对象(key)和线程的变量副本(value)
3)Thread内部的Map有ThreadLocal维护,ThreadLocalMap中变量副本的存取有ThreadLocal实例对象操作
4)对于不同的线程,每次获取变量副本,别的线程都不能获取当前线程的变量副本,每个变量副本都形成了隔离
-
JDk1.8中ThreadLocal设计相较于早期ThreadLocal设计的好处:
- 内存优化,每一个Map存储的Entry数量都变少了。因为早期ThreadLocalMap中Entry的数量取决于线程的数量,每一个线程都有与之对应的Entry;而JDK1.8中Entry的数量取决于ThreadLocal的数量,一个ThreadLocal对应一个Entry
- GC优化,早期Thead销毁,ThreadLocalMap对象仍有可能会存在,但在JDk1.8中当Thread销毁时,ThreadLocalMap也会随之销毁,从而减少了内存的使用
- 数据结构优化:在 JDK 8 中,ThreadLocal 引入了一个名为 InternalThreadLocalMap 的类来存储线程的局部变量副本。这种改变提高了对局部变量的访问效率。由于 InternalThreadLocalMap 使用开放寻址法解决哈希冲突,并且引入了数组长度和 size 字段,相较于之前的版本,使得 ThreadLocal 在并发环境下的性能得到了明显的提升。
4.2 ThreadLocal常用方法分析
方法声明 | 描述 |
---|---|
protected T initialValue() | 返回当前线程局部变量的初始值 |
public void set( T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
4.2.1 set原理分析
-
set
set方法执行流程:
A. 首先获取当前线程
B. 然后获取与当前线程绑定的Map
C. Map不为空,直接设置当前的值,key为ThreadLocal,value为传入的值
D. Map为空,创建应该Map,然后设置值
/*** 设置当前线程对应的ThreadLocal的值* @param value 将要保存到当前线程对应ThreadLocal中的值*/public void set(T value) {// 获取当前线程Thread t = Thread.currentThread();// 获取当前线程对应ThreadLocalMap对象ThreadLocalMap map = getMap(t);if (map != null)// map存在就设置值map.set(this, value);else// map不存在,就创建map然后设置值createMap(t, value);}/*** 获取当前线程Thread对应维护的ThreadLocalMap对象* @param t 当前线程* @return 当前线程对应维护的ThreadLocalMap对象*/ThreadLocal.ThreadLocalMap getMap(Thread t) {// threadLocals是Thread对应的一个成员变量return t.threadLocals;}/*** 为当前线程创建一个ThreadLocalMap对象* @param t 当前线程* @param firstValue 将要保存到当前线程对应ThreadLocal中的值*/void createMap(Thread t, T firstValue) { // 这里的this,表示调用ThreadLocalMap()方法的ThreadLocal对象(也就是现在new出来的这个ThreadLocal对象)t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);}
ThreadLocalMap的构造方法:
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {// 初始化一个Entrytable = new Entry[INITIAL_CAPACITY];// 计算key应该存在的位置int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);// 将Entry放到指定的位置table[i] = new Entry(firstKey, firstValue);size = 1;// 设置数组大小 16*2/3=10,类似于HashMap中的 0.75*16=12setThreshold(INITIAL_CAPACITY);}
4.2.2 get原理分析
-
get
get方法执行流程:
A. 首先获取当前线程,并根据当前线程获取一个Map
B. 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)
C. 如果Map为空,则给该线程创建 Map,并设置初始值
/*** 获取当前线程存入ThreadLocal中的值* @return value*/public T get() {// 获取当前线程Thread t = Thread.currentThread();// 获取当前线程对应的ThreadLocalThreadLocalMap map = getMap(t);if (map != null) {// 获取EntryThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {// 如果Entry不为空,直接返回当前线程存入的值@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}// ThreadLocalMap为空/*初始化 : 有两种情况有执行当前代码第一种情况: map不存在,表示此线程没有维护的ThreadLocalMap对象第二种情况: map存在, 但是没有与当前ThreadLocal关联的entry*/return setInitialValue();}/*** 初始化** @return the initial value 初始化后的值*/private T setInitialValue() {// 调用initialValue获取初始化的值// 此方法可以被子类重写, 如果不重写默认返回nullT value = initialValue();// 获取当前线程对象Thread t = Thread.currentThread();// 获取此线程对象中维护的ThreadLocalMap对象ThreadLocalMap map = getMap(t);// 判断map是否存在if (map != null)// 存在则调用map.set设置此实体entrymap.set(this, value);else// 1)当前线程Thread 不存在ThreadLocalMap对象// 2)则调用createMap进行ThreadLocalMap对象的初始化// 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中createMap(t, value);// 返回设置的值valuereturn value;}
initialValue
方法源码: A. 首先获取当前线程,并根据当前线程获取一个Map
B. 如果获取的Map不为空,则移除当前ThreadLocal对象对应的entry
/*** 删除当前线程中保存的ThreadLocal对应的实体entry*/public void remove() {// 获取当前线程对象中维护的ThreadLocalMap对象ThreadLocalMap m = getMap(Thread.currentThread());// 如果此map存在if (m != null)// 存在则调用map.remove// 以当前ThreadLocal为key删除对应的实体entrym.remove(this);}
4.2.3 remove原理分析
A. 首先获取当前线程,并根据当前线程获取一个Map
B. 如果获取的Map不为空,则移除当前ThreadLocal对象对应的entry
/*** 删除当前线程中保存的ThreadLocal对应的实体entry*/public void remove() {// 获取当前线程对象中维护的ThreadLocalMap对象ThreadLocalMap m = getMap(Thread.currentThread());// 如果此map存在if (m != null)// 存在则调用map.remove// 以当前ThreadLocal为key删除对应的实体entrym.remove(this);}
此方法的作用是 返回该线程局部变量的初始值。
(1) 这个方法是一个延迟调用方法,从上面的代码我们得知,在set方法还未调用而先调用了get方法时才执行,并且仅执行1次。
(2)这个方法缺省实现直接返回一个null
。
(3)如果想要一个除null之外的初始值,可以重写此方法。(备注: 该方法是一个protected
的方法,显然是为了让子类覆盖而设计的)
4.3 ThreadLocalMap源码分析
ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现
4.3.1 ThreadLocal基本结构
Alt+7
可以查看到ThreadLocal的内部结构
-
成员变量:
跟HashMap类似,
INITIAL_CAPACITY
代表这个Map的初始容量;table
是一个Entry
类型的数组,用于存储数据;size
代表表中的存储数目;threshold
代表需要扩容时对应size
的阈值/*** 初始容量 —— 必须是2的整次幂*/private static final int INITIAL_CAPACITY = 16;/*** 存放数据的table,Entry类的定义在下面分析* 同样,数组长度必须是2的整次幂。*/private Entry[] table;/*** 数组里面entrys的个数,可以用于判断table当前使用量是否超过阈值。*/private int size = 0;/*** 进行扩容的阈值,表使用量大于它的时候进行扩容。*/private int threshold; // Default to 0
-
存储结构:
在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。不过Entry中的key只能是ThreadLocal对象,这点在构造方法中已经限定死了。另外,Entry继承WeakReference,也就是key(ThreadLocal)是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑。
/** Entry继承WeakReference,并且用ThreadLocal作为key.* 如果key为null(entry.get() == null),意味着key不再被引用,* 因此这时候entry也可以从table中清除。*/ static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;} }
在ThreadLocalMap中,也是用
Entry
来保存K-V
结构数据的。不过Entry中的key只能是ThreadLocal对象,这点在构造方法中已经限定死了。另外,Entry
继承WeakReference
,也就是key
(ThreadLocal)是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑。
5、ThreadLocal存在的问题
5.1 内存泄漏问题
有些程序员在使用 ThreadLocal 的过程中会发现有内存泄漏的情况发生,就猜测这个内存泄漏跟 Entry 中使用了弱引用的key有关系。这个理解其实是不对的。我们先来回顾这个问题中涉及的几个名词概念,再来分析问题。
-
区分内存溢出和内存泄露两个概念
-
Memory overflow
:内存溢出,是指没有足够的内存提供申请者使用。 -
Memory leak
:内存泄漏,是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出。常见导致内存泄露的原因有:程序中存在错误的对象引用处理、资源未正确关闭或释放等问题
总结:内存溢出是申请分配的内存超过了当前系统最大内存限制,内存泄露是指已分配的内存得不到及时的释放导致内存逐渐增加,最终影响系统性能甚至导致系统崩溃
-
-
弱引用相关概念、
Java中的引用有4种类型: 强、软、弱、虚,强引用是默认的引用类型,不会被垃圾回收;软引用在内存不足时可能被回收;弱引用在垃圾回收时会被回收;虚引用几乎没有实际用途,主要用于跟踪对象被垃圾回收的活动。
-
强引用(Strong Reference):
-
强引用是默认的引用类型,在代码中直接使用对象时,就是强引用。
-
强引用指向的对象不会被垃圾回收器回收,即使内存不足时也不会进行回收。
-
只有当没有任何强引用指向一个对象时,该对象才会被判定为可回收,且可能在之后被垃圾回收器回收。
-
-
软引用(Soft Reference):
-
软引用用于描述一些还有用但非必需的对象。
-
当内存不足时,垃圾回收器会根据软引用来决定是否回收对象。
-
只有当垃圾回收器认为内存不足时,才会回收软引用指向的对象。
-
-
弱引用(Weak Reference):
-
弱引用用于描述非必需的对象。
-
当垃圾回收器进行垃圾回收时,无论内存是否充足,都会回收弱引用指向的对象。
-
弱引用通常用于实现缓存或者观察者模式等场景。
-
-
虚引用(Phantom Reference):
-
虚引用是最弱的一种引用关系,几乎没有实际用途。
-
虚引用的主要作用是跟踪对象被垃圾回收的活动。
-
虚引用必须与引用队列(ReferenceQueue)联合使用,当对象被垃圾回收时会被放入引用队列中。
-
-
当前ThreadLocal内存泄露这个问题主要涉及到强引用和弱引用
-
如果 Entry 的 key 使用了强引用,会出现内存泄漏吗?我们来分析看看
此时ThreadLocal的内存图(实线表示强引用,虚线表示弱引用)如下:
- 假设在业务代码中使用完 ThreadLocal ,ThreadLocal Ref被回收了。
- 但是因为 ThreadLocalMap 的 Entry 强引用了 ThreadLocal,造成 ThreadLocal 无法被回收。
- 在没有手动删除这个 Entry 以及 CurrentThread 依然运行的前提下,始终有强引用链 ThreadRef→CurrentThread→ThreadLocalMap→Entry ,Entry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。
- 也就是说,ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的。
-
那么ThreadLocalMap 中的 key 使用了弱引用,会出现内存泄漏吗?同样的我们来分析一下
此时ThreadLocal的内存图(实线表示强引用,虚线表示弱引用)如下:
-
同样假设在业务代码中使用完 ThreadLocal ,ThreadLocal Ref被回收了。
-
由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向 Threadlocal 实例, 所以 Threadlocal 就可以顺利被 GC 回收,此时 Entry 中的key=null。
-
但是在没有手动删除这个 Entry 以及 CurrentThread 依然运行的前提下,也存在有强引用链
Thread Ref→CurrentThread→ThreadLocalMap→Entry,Entry就不会被回收,此时Entry中的key也就是ThreadLocal已经被GC了,但是 value 由于强引用链的存在没有被回收,所以依然会导致Entry内存泄漏。
-
也就是说,ThreadLocalMap中的 key 使用了弱引用, 也有可能内存泄漏。
-
-
比较以上两种情况,我们就会发现,内存泄漏的发生跟ThreadLocalMap中的 key 是否使用弱引用是没有关系的。那么内存泄漏的的真正原因是什么呢?
其实不难看出,在以上两种内存泄漏的情况中,都有两个前提:
- 没有手动删除这个Entry
- CurrentThread依然运行
这两个前提的存在,会使得 Thread Ref→CurrentThread→ThreadLocalMap→Entry 这条强引用链一直存在,从而导致出现内存泄露,那么我们该如何较好的避免出现内存泄露问题呢?
-
针对第一点,只要在使用完ThreadLocal,调用其remove方法删除对应的Entry,就能避免内存泄漏;
-
针对第二点,由于ThreadLocalMap是Thread的一个属性,被当前线程所引用,所以它的生命周期跟Thread一样长。那么在使用完 ThreadLocal 的使用,如果当前 Thread 也随之执行结束,ThreadLocalMap自然也会被 GC 回收,从根源上避免了内存泄漏
综上,导致ThreadLocal发生内存泄露的原因有两个:
- 使用完ThreadLocal后没有手动删除对应key
- ThreadLocalMap的生命周期跟Thread一样长
针对第二条,我们没有更好的处理方案,特别是在使用线程池时,线程使用完不会直接被销毁也就是说CurrentThread会一直存在,但是ThreadLocal的创造者提供了一个解决方案(不能说解决,只能说降低内存泄露的概率):ThreadLocalMap使用弱引用,key为null时调用
set
/getEntry
方法会自动将value置为null。为什么这样能够降低内存泄露的概率呢?在 ThreadLocalMap 中的 set/getEntry 方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。这就意味着使用完 ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用 remove 方法,弱引用比强引用可以多一层保障:弱引用的 ThreadLocal 会被回收,对应的value在下一次ThreadLocalMap调用 set、get、remove中的任一方法的时候会被清除,从而大概率避免内存泄漏。
5.2 Hash冲突问题
-
什么是Hash冲突?
哈希冲突(Hash Collision)是指不同的输入数据经过哈希函数计算后得到相同的哈希值。哈希函数将输入数据映射到一个固定大小的哈希值,而在实际情况下,输入数据的数量通常远大于哈希值的范围,这就导致了可能存在多个不同的输入数据具有相同的哈希值。
-
常见解决Hash冲突的方案有:
- 链地址法(Chaining):使用链表或者其他数据结构,在哈希冲突的位置上存储一个链表,将冲突的元素链接在一起。
- 开放地址法(Open Addressing):在发生哈希冲突时,通过探测序列的方式,尝试找到下一个可用的空槽位来存储冲突的元素。
- 再哈希法(Rehashing):当发生冲突时,使用另外一个哈希函数再次计算哈希值,直到找到一个空槽位来存储冲突的元素。
- 建立更好的哈希函数:通过设计更好的哈希函数,降低哈希冲突的概率,使得哈希值的分布更加均匀。
hash冲突的解决是Map中的一个重要内容。我们以hash冲突的解决为线索,来研究一下ThreadLocalMap的核心源码
1)从 ThreadLocal 的 set
方法的源码作为入口,一步一步的深入探究
public void set(T value) {Thread t = Thread.currentThread();ThreadLocal.ThreadLocalMap map = getMap(t);if (map != null)//调用了ThreadLocalMap的set方法map.set(this, value);elsecreateMap(t, value);}ThreadLocal.ThreadLocalMap getMap(Thread t) {return t.threadLocals;}void createMap(Thread t, T firstValue) {//调用了ThreadLocalMap的构造方法t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);}
这个方法我们前面已经分析过, 其作用是设置当前线程绑定的局部变量 :
a. 首先获取当前线程,并根据当前线程获取一个Map
b. 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key,这里调用了ThreadLocalMap的set方法)
c. 如果Map为空,则给该线程创建 Map,并设置初始值(这里调用了ThreadLocalMap的构造方法)
ThreadLocal 的 set 方法涉及到了 ThreadLocalMap 的 set 方法和构造方法,我们接着分析这两个方法
2)ThreadLocalMap的构造方法ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)
/** firstKey : 本ThreadLocal实例(this)* firstValue : 要保存的线程本地变量*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {//初始化tabletable = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];//计算索引(重点代码)int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//设置值table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);size = 1;//设置阈值setThreshold(INITIAL_CAPACITY);}
可以看到构造函数首先创建一个长度为16的 Entry 数组,然后计算出 firstKey 对应的索引,然后存储到 table 中,并设置 size 和threshold,我们重点分析重点分析int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)
这行代码
a. 关于firstKey.threadLocalHashCode
:
private final int threadLocalHashCode = nextHashCode();private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}//AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减,适合高并发情况下的使用private static AtomicInteger nextHashCode = new AtomicInteger();//特殊的hash值private static final int HASH_INCREMENT = 0x61c88647;
这里定义了一个AtomicInteger
类型,每次获取当前值并加上HASH_INCREMENT
,HASH_INCREMENT = 0x61c88647
,这个值跟斐波那契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里, 也就是Entry[] table
中,这样做可以尽量避免 hash 冲突。
b. 关于& (INITIAL_CAPACITY - 1)
计算 hash 的时候里面采用了hashCode & (size - 1)
的算法,这相当于取模运算hashCode % size
的一个更高效的实现,(size - 1)
的作用是保证计算得到的索引值在哈希表的范围之内,因为 (size - 1)
的二进制表示都是最低位全为 1,其它位全为 0。通过与运算,可以将 hashCode
的高位(对应大数值)的影响消除,只保留低位的部分,确保索引值符合哈希表的大小范围。
注意:该算法要求哈希表的大小必须是 2 的幂次方,即 size
必须是 2、4、8、16 等等,才保证在索引不越界的前提下,使得hash发生冲突的次数减小。这是因为 (size - 1)
的二进制表示中,除了最低位为 1,其它位都是 0,这样才能保证按位与运算的结果在有效的索引范围内
3)ThreadLocalMap 中的 set 方法
private void set(ThreadLocal<?> key, Object value) {ThreadLocal.ThreadLocalMap.Entry[] tab = table;int len = tab.length;// 计算索引(重点代码,前面分析过了)int i = key.threadLocalHashCode & (len-1);// 使用线性探测法查找元素(重点代码)for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();//ThreadLocal 对应的 key 存在,直接覆盖之前的值if (k == key) {e.value = value;return;}// key为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了,// 当前数组中的 Entry 是一个陈旧(stale)的元素if (k == null) {//用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏replaceStaleEntry(key, value, i);return;}}//ThreadLocal对应的key不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的Entry。tab[i] = new Entry(key, value);int sz = ++size;/** cleanSomeSlots用于清除那些e.get()==null的元素,* 这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置null。* 如果没有清除任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行 * rehash(执行一次全表的扫描清理工作)*/if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();
}/*** 获取环形数组的下一个索引*/private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);}
a. 首先还是根据 key 计算出索引 i,然后查找索引位置上的Entry
b. 若是Entry已经存在并且key等于传入的key,那么这时候直接给这个Entry赋新的value值,
c. 若是Entry存在,但是key为null,value不为null,则说明之前的ThreadLocal对象已经被GC了,这个时候需要调用 replaceStaleEntry
来更换这个 key 为空的Entry。
这里关于这里为什么要单独使用一个方法 replaceStaleEntry 来将Entry的value替换成null的原因:首先,ThreadLocalMap 中的 Entry 对象存储了线程局部变量的值。在多线程环境下,如果直接将 Entry 的 value 设置为 null,可能会导致其他线程无法正确获取到自己的线程局部变量值,从而出现线程间数据错乱或不一致的情况。其次,由于 ThreadLocalMap 是使用弱引用来引用 ThreadLocal 对象的,当 ThreadLocal 对象被 GC 后,对应的 Entry 中的 key 会变为 null。这时如果直接将 Entry 的 value 设置为 null,那么就无法区分是因为 GC 导致的 key 为 null,还是本来就是 key 和 value 都为 null 的情况
d. 不断循环检测(利用线性探测法),直到遇到为null的地方,这时候要是还没在循环过程中return,那么就在这个null的位置新建一个Entry,并且插入,同时size增加1。
e. 最后调用cleanSomeSlots,清理key为null的Entry,最后返回是否清理了Entry,接下来再判断sz 是否>= thresgold达到了rehash的条件,达到的话就会调用rehash函数执行一次全表的扫描清理。
知识拓展:线性探测法
线性探测法(Linear Probing)是一种解决哈希冲突的开放地址法之一。当发生哈希冲突时,线性探测法通过逐个检查下一个槽位,直到找到一个空闲的槽位来存储冲突的元素,若整个空间都找不到空余的地址,则产生溢出。
具体实现时,当在哈希表中插入一个元素时,如果计算得到的索引位置已经被占用,就会顺序地往后探测直到找到一个空闲的槽位。探测的方式是通过对当前索引位置进行加 1 操作,并将结果与哈希表的大小取模,以保证索引在有效范围内。如果遇到了已经被占用的槽位,则继续进行探测,直到找到一个空闲的槽位为止。
举个例子,假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。按照上面的描述,可以把Entry[] table看成一个环形数组。
相关面试题
-
为什么使用ThreadLocal而不直接使用Session存储用户id?
-
更加轻量级:使用ThreadLocal不需要创建和维护会话,因此可以在某些情况下更加轻量级。
降低了层与层之间的耦合性,Service层要想获取用户id,需要从Controller层中获取Session对象,然后传入Service层中,而使用ThreadLocal后,可以直接获取Controller层无需传递多余的参数;其次,在一些类中可能无法获取Session对象的,比如公共字段填充类中,此时就无法获取用户id,这时候就可以使用ThreadLocal更加便捷
-
更高的性能:在高并发环境下,使用ThreadLocal可以减少上下文切换的次数,从而提高性能。
-
更好的安全性:由于ThreadLocal中的变量只能在当前线程中访问,因此不存在多线程竞争的问题,从而可以提高安全性。
-
参考文章:
使用ThreadLocal - 廖雪峰
ThreadLocal使用与原理 - 傲丙
ThreadLocal源码解析 - 程序员小潘的博客
SimpleDateFormat多线程下不安全!!!解决之道 - 腾讯云开发者社区-腾讯云 (tencent.com)
Java学习社区
关于yyyy-MM-dd格式日期字符串,解析成LocalDateTime遇到的问题
ThreadLocal源码深入剖析 - 简书 (jianshu.com)
HashMap的数学原理 - 掘金 (juejin.cn)