Java多线程之ThreadLocal原理总结
作者:奔赴在自己的热爱中 发布时间:2023-11-02 22:58:36
1、什么是 ThreadLocal:
ThreadLocal,即线程本地变量,如果你创建了一个变量,那么访问这个变量的每个线程都会有这个变量的本地拷贝,多个线程操作这个变量的时候,实际操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题
ThreadLocal 适用于无状态,副本变量独立后不影响业务逻辑的高并发场景,如果业务逻辑强依赖于变量副本,则不适合用 ThreadLocal 解决,需要另寻解决方案
应用场景:
数据库连接池会话管理中使用
2、ThreadLocal 的数据结构:
在 JDK8 中,每个线程 Thread 内部都维护了一个 ThreadLocalMap 的数据结构,ThreadLocalMap 中有一个由内部类 Entry 组成的 table 数组,Entry 的 key 就是线程的本地化对象 ThreadLocal,而 value 则存放了当前线程所操作的变量副本。每个 ThreadLocal 只能保存一个副本 value,并且各个线程的数据互不干扰,如果想要一个线程保存多个副本变量,就需要创建多个ThreadLocal。
一个 ThreadLocal 的值,会根据线程的不同,分散在 N 个线程中,所以获取 ThreadLocal 的 value,有两个步骤:
第一步,根据线程获取 ThreadLocalMap
第二步,根据自身从 ThreadLocalMap 中获取值,所以它的 this 就是 Map 的 Key
当执行 set() 方法时,其值是保存在当前线程的 ThreadLocal 变量副本中
当执行get() 方法中,是从当前线程的 ThreadLocal 的变量副本获取。
所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了线程的隔离,互不干扰。
3、ThreadLocal 的核心方法:
ThreadLocal 对外暴露的方法有4个:
1.initialValue()方法:返回为当前线程初始副本变量值。
2.get()方法:获取当前线程的副本变量值。
3.set()方法:保存当前线程的副本变量值。
4.remove()方法:移除当前前程的副本变量值
1、set()方法:
// 设置当前线程对应的ThreadLocal值
public void set(T value) {
Thread t = Thread.currentThread(); // 获取当前线程对象
ThreadLocalMap map = getMap(t);
if (map != null) // 判断map是否存在
map.set(this, value);
// 调用map.set 将当前value赋值给当前threadLocal。
else
createMap(t, value);
// 如果当前对象没有ThreadLocalMap 对象。
// 创建一个对象 赋值给当前线程
}
// 获取当前线程对象维护的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 给传入的线程 配置一个threadlocals
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
执行流程:
1.获得当前线程,根据当前线程获得 map。
2.如果 map 不为空,则将参数设置到 map 中,当前的 Threadlocal 作为 key。
3.如果 map 为空,则给该线程创建 map,设置初始值。
2、get()方法:
public T get() {
Thread t = Thread.currentThread();//获得当前线程对象
ThreadLocalMap map = getMap(t);//线程对象对应的map
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);// 以当前threadlocal为key,尝试获得实体
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果当前线程对应map不存在
// 如果map存在但是当前threadlocal没有关连的entry。
return setInitialValue();
}
// 初始化
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
执行流程:
(1)先尝试获得当前线程,再根据当前线程获取对应的 map
(2)如果获得的 map 不为空,以当前 threadlocal 为 key 尝试获得 entry
(3)如果 entry 不为空,返回值。
(4)如果 2 跟 3 出现无法获得,则通过 initialValue 函数获得初始值,然后给当前线程创建新 map
3、remove()方法:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
执行流程:
首先尝试获取当前线程,然后根据当前线程获得map,从map中尝试删除enrty。
4、initialValue() 方法:
protected T initialValue() {
return null;
}
执行流程:
(1)如果没有调用 set() 直接 get(),则会调用此方法,该方法只会被调用一次,
(2)默认返回一个缺省值null,如果不想返回null,可以Override 进行覆盖。
4、ThreadLocal 的哈希冲突的解决方法:线性探测
和 HashMap 不同,ThreadLocalMap 结构中没有 next 引用
ThreadLocalMap 中解决哈希冲突的方式并非链表的方式,而是采用线性探测的方式,当发生哈希冲突时就将步长加1或减1,寻找下一个相邻的位置
流程说明:
1.根据 ThreadLocal 对象的 hash 值,定位到 table 中的位置 i;
2.如果当前位置是 null,就初始化一个 Entry 对象放在位置 i 上;
3.如果位置 i 已经有 Entry 对象了,如果这个 Entry 对象的 key 与即将设置的 key 相同,那么重新设置 Entry 的 value;
4.如果位置 i 的 Entry 对象和 即将设置的 key 不同,那么寻找下一个空位置;
5、ThreadLocal 的内存泄露:
在使用 ThreadLocal 时,当使用完变量后,必须手动调用 remove() 方法删除 entry 对象,否则会造成 value 的内存泄露,严格来说,ThreadLocal 是没有内存泄漏问题,有的话,那也是忘记执行 remove() 引起的
内存泄露的根本原因在于 ThreadLocalMap 的生命周期与当前线程 CurrentThread 的生命周期相同,且 ThreadLocal 使用完没有进行手动删除导致的
ThreadLocal 的内存泄露与强弱引用无关,那么为什么还要用弱引用呢?
(1)Entry 中的 key(Threadlocal)是弱引用,目的是将 ThreadLocal 对象的生命周期跟线程周期解绑,用 WeakReference 弱引用关联的对象,只能生存到下一次垃圾回收之前,GC发生时,不管内存够不够,都会被回收。
(2)当我们使用完 ThreadLocal,而 Thread 仍然运行时,即使忘记调用 remove() 方法, 弱引用也会比强引用多一层保障:当 GC 发生时,弱引用的 ThreadLocal 被收回,那么 key 就为 null 了。而 ThreadLocalMap 中的 set()、get() 方法,会针对 key == null (也就是 ThreadLocal 为 null) 的情况进行处理,如果 key == null,则系统认为 value 也应该是无效了应该设置为 null,也就是说对应的 value 会在下次调用 ThreadLocal 的 set()、get() 方法时,执行底层 ThreadLocalMap 中的 expungeStaleEntry() 方法进行清除无用的 value,从而避免内存泄露。
6、ThreadLocal 的应用场景:
(1)Hibernate 的 session 获取:每个线程访问数据库都应当是一个独立的 session 会话,如果多个线程共享同一个 session 会话,有可能其他线程关闭连接了,当前线程再执行提交时就会出现会话已关闭的异常,导致系统异常。
使用 ThreadLocal 的方式能避免线程争抢session,提高并发安全性。
(2)Spring 的事务管理:事务需要保证一组操作同时成功或失败,意味着一个事务的所有操作需要在同一个数据库连接上,Spring 采用 Threadlocal 的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时采用这种方式可以使业务层使用事务时不需要感知并管理 connection 对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复
7、如果想共享线程的 ThreadLocal 数据怎么办 ?
使用 InheritableThreadLocal 可以实现多个线程访问 ThreadLocal 的值
我们在主线程中创建一个 InheritableThreadLocal 的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。
private void test() {
final ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("主线程的ThreadLocal的值");
Thread t = new Thread() {
@Override
public void run() {
super.run();
Log.i( "我是子线程,我要获取其他线程的ThreadLocal的值 ==> " + threadLocal.get());
}
};
t.start();
}
8、为什么一般用 ThreadLocal 都要用 static?
ThreadLocal 能实现线程的数据隔离,不在于它自己本身,而在于 Thread 的 ThreadLocalMap,所以,ThreadLocal 可以只实例化一次,只分配一块存储空间就可以了,没有必要作为成员变量多次被初始化。
来源:https://blog.csdn.net/qq_51711443/article/details/130032575


猜你喜欢
- 1. 背景Java Persistence with Hibernate 在12.2.1小节使用如下例子描述 n+1查询问题:List<
- 项目已经添加了svn,但右键项目时找不到Svn选择但在VCS中却有,很奇怪这个问题是svn的根路径与当前IDEA打开的项目路径不一致的原因在
- java线程同步原理java会为每个object对象分配一个monitor,当某个对象的同步方法(synchronizedmethods)被
- 本文为大家分享了java门禁系统面向对象程序设计,供大家参考,具体内容如下下面是对门禁系统的介绍:需求1、某公司安装了电子门,要设计门禁系统
- 1.概述MybatisPlus是国产的第三方插件, 它封装了许多常用的CURDapi,免去了我们写mapper.xml的重复劳动,这里介绍了
- 由于工作和之前学习的原因,我需要同时使用Java和C#两门语言。这几年,我也一直在使用J2EE平台和.NET平台做开发。说起C#和Java这
- 使用JDBC时,我们都会很自然得使用下列语句:Class.forName("com.mysql.jdbc.Driver"
- 在进行winform开发过程中有时候会需要关闭其他程序或者关闭进程,以前写过一篇相关介绍的文章,今天有同事问起来,于是在次翻出来和大家分享一
- 实践过程效果代码public partial class Form1 : Form{ public Form1()
- 本文实例讲述了C#字符串加密解密方法。分享给大家供大家参考。具体如下:#region 加密解密static string encryptKe
- 前言Graalvm通过静态分析提前编译来为Java应用程序构建高度优化的本机可执行文件,这就需要在编译时就知道所有的程序类型,而java中的
- 1、字符数组的定义与初始化字符数组的初始化,最容易理解的方式就是逐个字符赋给数组中各元素。char str[10]={ 'I'
- 本文实例讲述了C#实现对字符串进行大小写切换的方法。分享给大家供大家参考。具体实现方法如下:C#代码如下:public class Prog
- 废话不多说,直接给大家贴代码了,具体代码如下所示:package com.luo.wctweb.util; import java.awt
- 本文为大家分享了Android AIDL实现两个APP间的跨进程通信实例,供大家参考,具体内容如下1 Service端创建首先需要创建一个A
- 程序在32位操作系统上运行正常,在64位操作系统上运行读卡功能提示”试图加载格式不正确“。-------------------------
- 本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下:我们需要获取Android手机或Pad的屏幕的
- 模仿网易新闻客户端阅读偏好的频道选择,先看实现的页面: 直接上代码:import android.content.res.Resources
- 本文实例讲述了Android编程解析XML文件的方法。分享给大家供大家参考,具体如下:前言在学习Android的Framework层源码时,
- 本文实例讲述了C# winform实现右下角弹出窗口结果的方法。分享给大家供大家参考,具体如下:using System.Runtime.I