JDK8 ThreadLocal 源码解析与最佳实践

news2024/12/23 8:28:52

文章目录

  • 用法
    • Example1
    • Example2
  • Springboot @Transcation 注解的原理
  • Entry 的 Key 设置为弱引用有什么好处
  • 内存泄漏问题
  • 为什么 ThreadLocal 不需要 ReferenceQueue
    • get()
    • getEntry
    • getEntryAfterMiss

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable.ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g.,a user ID or Transaction ID).

该类提供线程局部变量。这些变量与普通变量的不同之处在于,每个访问一个变量(通过其 get 或 set 方法)的线程都有自己的、独立初始化的变量副本。ThreadLocal 实例通常是类中的私有静态字段,希望将状态与线程关联(例如,用户 ID 或事务 ID)。

每个线程中维护一个 ThreadLocalMap,ThreadLocalMap 虽然名字里有 Map,实际上是 Entry 数组。Entry 数组中 Entry 的 key 是 ThreadLocal,Value 是 保存的属性。下面以一段代码举例:

    public static void main(String[] args) {        
        ThreadLocal<Integer> tl = new ThreadLocal<>();        
        tl.set(1);    
    }

Pasted image 20221211190622

用法

Example1

import java.util.concurrent.TimeUnit;

public class ThreadLocal2 {
    static ThreadLocal<Person> tl = new ThreadLocal<>();
    
    public static void main(String[] args) {
        new Thread(()->{
            try {
                //先睡两秒
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            System.out.println(tl.get());
        }).start();
        
        new Thread(()->{
            try {
                //先睡一秒
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            tl.set(new Person());
        }).start();
    }
    
    static class Person {
        String name = "zhangsan";
    }
}

输出:

null

Example2

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadId {
    // Atomic integer containing the next thread ID to be assigned
    private static final AtomicInteger nextId = new AtomicInteger(0);

    // Thread local variable containing each thread's ID
    private static final ThreadLocal<Integer> threadId =
            ThreadLocal.withInitial(nextId::getAndIncrement);

    // Returns the current thread's unique ID, assigning it if necessary
    public static int get() {
        return threadId.get();
    }

输出:

get() = 0

Springboot @Transcation 注解的原理

@Transcation 注解实现原理是基于 AOP 编程思想。AOP 编程思想是将与业务无关的功能抽离出来,并在业务处理前后进行插入。在 Springboot 源码中,@Transcation 注解就是使用 AOP 编程思想实现的。
当一个方法上有 @Transcation 注解时,在 Springboot 源码中会为这个方法生成一个代理对象。在这个代理对象中,会添加两个通知:前置通知和后置通知。前置通知会在真实方法执行前执行,后置通知会在真实方法执行后执行。
在前置通知中,会开启一个事务,并将事务挂起。在真实方法执行完毕后,会在后置通知中提交事务。如果真实方法抛出异常,则会在后置通知中回滚事务。
这样,当一个方法上有 @Transcation 注解时,方法的执行过程就会被包装在一个事务中,从而可以实现事务的控制。
在 Spring 中,每个线程都有一个 ThreadLocal 对象,可以用来存储线程私有的数据。在 Spring 的事务管理器中,会将当前线程所使用的数据源放入 ThreadLocal 对象中。所以,在前置通知中,可以从 ThreadLocal 对象中获取当前线程所使用的数据源。
获取数据源:

DataSource dataSource = TransactionSynchronizationManager.getResource(dataSource);

设置数据源:
Pasted image 20221211175302

Entry 的 Key 设置为弱引用有什么好处

我们先假设不是弱引用,是强引用。当 tl 在外部的强引用被回收以后,因为 ThreadLocalMap 的 Entry 的 Key 还是强引用,只要线程不结束,这个 ThreadLocal 一直不会被回收。
如果是弱引用,tl 在外部的强引用被回收以后,GC 时就会回收这个弱引用的 ThreadLocal 对象。但是此时 Value 还没有被回收,这就导致了内存泄漏问题。
Pasted image 20221211183100

内存泄漏问题

ThreadLocal 类本身并不会导致内存泄漏。但是,如果使用不当,可能会导致内存泄漏。上文中就描述了 Value 没有被回收导致的内存泄漏问题,那么我们看什么时候 Value 会被回收。
Pasted image 20221210214615

  1. 线程结束,ThreadLocalMap 清除
    由于在 JAVA 中线程对象与 ThreadLocal 对象绑定在一起,所以只要线程没了,空间自然回收。因为线程对象回收时,ThreadLocalMap 的强引用也没了,所以内部全部回收。
  2. 线程未结束
    1. 调用 remove 方法,释放 Entry 的内存。
    2. 调用 get 和 set 方法时,释放 key == null 的 Entry 的内存。

remove、get和set方法都会调用 expungeStaleEntry 方法来释放 value 的内存。

           // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

下面是 expungeStaleEntry 方法的源码。
Expunge a stale entry by rehashing any possibly colliding entries lying between staleSlot and the next null slot. This also expunges any other stale entries encountered before the trailing null. See Knuth, Section 6.4

  • staleSlot : index of slot known to have null key
     private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

若调用完 ThreadLocal 后,线程未结束,且没有调用 remove、set 和 get 方法。才会导致内存泄漏。虽然 get 和 set 时,会回收 value。但是我们不能依赖该机制回收内存,这只是个兜底的方案。开发中建议使用完 ThreadLocal 后就 remove 即可。
Pasted image 20221211185504
Pasted image 20221211185817

为什么 ThreadLocal 不需要 ReferenceQueue

为什么 [[01 JDK8 源码/WeakHashMap]] 需要 ReferenceQueue 来保存需要被回收的 Entry,而 ThreadLocal 不需要。
因为 Entry 对象的强引用本身就保存在 ThreadLocalMap 中了,只要 Entry 的 key 等于 null,就表明需要回收这个 Entry。具体参考源码中 getEntryAfterMiss 方法:

if (k == null)
    expungeStaleEntry(i);

get()

Returns the value in the current thread’s copy of this thread-local variable. If the variable has no value for the current thread, it is first initialized to the value returned by an invocation of the initialValue method.

    public T get() {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 每个线程维护一个 ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

getEntry

        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

getEntryAfterMiss

        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

路径规划|多目标海洋捕食者算法(MOMPA)求解最短路径问题(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️❤️&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清…

LabVIEW在面向对象编程中利用硬件抽象层(HAL)设计1

LabVIEW在面向对象编程中利用硬件抽象层(HAL)设计1 LabVIEW面向对象编程(OOP)采用仪器为中心的硬件抽象层(HAL)&#xff0c;使用面向对象的设计模式&#xff0c;可以部署一个仪器重用库&#xff0c;该库可以随着需求和仪器趋势的变化而增长&#xff0c;同时在不可避免的硬件过…

【Redis】主从复制

一、简介 1、什么是主从复制 主机数据更新后根据配置和策略&#xff0c;自动同步到备机的master/slave机制&#xff0c;Matser以写为主&#xff0c;Slave以读为主&#xff0c;简单来说如下图 2、主从复制的好处 读写分离&#xff1a;Matser以写为主&#xff0c;Slave以读为主…

指针的初步认识

&#x1f3d6;️作者&#xff1a;malloc不出对象 ⛺专栏&#xff1a;《初识C语言》 &#x1f466;个人简介&#xff1a;一名双非本科院校大二在读的科班编程菜鸟&#xff0c;努力编程只为赶上各位大佬的步伐&#x1f648;&#x1f648; 目录前言一、指针是什么1.1 如何理解编址…

《深入分布式缓存-从原理到实践》笔记

《深入分布式缓存-从原理到实践》笔记 笔记作者&#xff1a;arthury.dy.lee 日期&#xff1a;2018.12.05 笔记只是本人觉得重要的部分的一些摘要或总结&#xff0c;更侧重于后5章。其它更详细内容&#xff0c;请自行买书阅读。 文章目录《深入分布式缓存-从原理到实践》笔记…

【Linux C】线程简单介绍

线程的概念 基本概念 进程&#xff1a;是指⼀个内存中运⾏的应⽤程序&#xff0c;每个进程都有⼀个独⽴的内存空间&#xff0c;⼀个应⽤程序可以同时运⾏多个进程&#xff1b;进程也是程序的⼀次执⾏过程&#xff0c;是系统运⾏程序的基本单位&#xff1b;系统运⾏⼀个程序即…

时序预测 | MATLAB实现具有外生回归变量的ARIMAX时间序列预测(含AR、MA、ARIMA、SARIMA、VAR对比)

时序预测 | MATLAB实现具有外生回归变量的ARIMAX时间序列预测(含AR、MA、ARIMA、SARIMA、VAR对比) 目录 时序预测 | MATLAB实现具有外生回归变量的ARIMAX时间序列预测(含AR、MA、ARIMA、SARIMA、VAR对比)预测效果基本介绍程序设计ARMAARMAARIMASARIMAARIMAXVAR参考资料预测效果…

LC-6260. 矩阵查询可获得的最大分数(最小堆,并查集+离线(海平面上升问题))【周赛323】

6260. 矩阵查询可获得的最大分数 难度困难7 给你一个大小为 m x n 的整数矩阵 grid 和一个大小为 k 的数组 queries 。 找出一个大小为 k 的数组 answer &#xff0c;且满足对于每个整数 queres[i] &#xff0c;你从矩阵 左上角 单元格开始&#xff0c;重复以下过程&#xf…

2022年了,你还没搞清楚箭头函数与普通函数的区别嘛?

目录 1.箭头函数简介 2.箭头函数与普通函数的区别 A.声明方式不同&#xff0c;匿名函数 B.this指向不同 C.箭头函数的this永远不会变&#xff0c;call、apply、bind也无法改变 D.箭头函数没有原型prototype E.箭头函数不能当成一个构造函数 F.箭头函数没有自己的argume…

javaSE(数据类型、运算、逻辑控制、方法)

1.初识Java JDK、JRE、JVM之间的关系&#xff1f; JDK(Java Development Kit):Java开发工具包&#xff0c;提供给Java程序员使用&#xff0c;包含了JRE&#xff0c;同时还包含了编译器javac与自带的调试工具Jconsole、jstack等。 JRE(Java Runtime Environment):Java运行时环…

LeetCode 1775. 通过最少操作次数使数组的和相等 --双指针

通过最少操作次数使数组的和相等 中等 174 相关企业 给你两个长度可能不等的整数数组 nums1 和 nums2 。两个数组中的所有值都在 1 到 6 之间&#xff08;包含 1 和 6&#xff09;。 每次操作中&#xff0c;你可以选择 任意 数组中的任意一个整数&#xff0c;将它变成 1 到 6 …

Android9.0以上系统安装Edxposed

说明&#xff1a;仅供学习使用&#xff0c;请勿用于非法用途&#xff0c;若有侵权&#xff0c;请联系博主删除 作者&#xff1a;zhu6201976 一、背景说明 Android9.0以前&#xff0c;Xposed框架可通过apk进行快速安装&#xff0c;github地址&#xff1a; GitHub - rovo89/Xpos…

CRACK:CAD Exchanger SDK 3.15.0/MAC/WIN/LINUX/Android

CAD Exchanger SDK用于读取、写入和可视化 3D CAD 文件的软件库 通过访问 CAD 和 BIM 数据&#xff0c;快速轻松地丰富您的 Web、服务器或桌面应用程序。Ω578867473 使用 CATIA、SOLIDWORKS、Creo、STEP、JT、IFC 以及来自 C、Python、C#、Java 和 JavaScript 的更多格式。 适…

人民日报强烈推荐的13本证书,含金量都很高!

人民日报每年都会推荐一些当代最具含金量的证书&#xff0c;并建议大学生在大学期间的时候着手准备&#xff0c;为毕业后的简历添加色彩。 本次&#xff0c;人民日报推荐的证书主要有下列13种&#xff1a; 01 CPA&#xff08;注册会计师&#xff09; 含金量&#xff1a;★★…

博客管理系统

大致思路 1. 引入的依赖 数据库 Maven Repository: mysql mysql-connector-java 5.1.47 (mvnrepository.com) <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --><dependency><groupId>mysql</groupId><artifactId>mys…

centos7用容器搭建svn仓库和管理页面

文章目录安装docker拉取 svn 仓库管理镜像创建仓库使用仓库安装docker 安装 docker 服务 yum install -y docker 修改 docker 服务配置&#xff0c;添加镜像拉取加速路径 vim /etc/docker/daemon.json javascript {"registry-mirrors": ["http://f1361db2.m.da…

【网页期末作业】基于HTML学校教育网页项目的设计与实现

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

更自动化的开发----给开发插上恣意飞翔的翅膀!

总结&#xff1a; 设置启动项&#xff1a;开机时候快速启动自己所需要的程序&#xff0c;根据程序数量不一样&#xff0c;每天节省可重复的劳动时间约为5s-1min(此处数据为自己瞎说 O(∩_∩)O哈哈~)&#xff1b; 使用生成代码的插件&#xff1a;开发使用easycode的插件&#…

m基于多D2D通信对和多蜂窝用户的LTE预编码技术matlab仿真

目录 1.算法描述 2.仿真效果预览 3.MATLAB核心程序 4.完整MATLAB 1.算法描述 LTE网络中采用MIMO技术增加系统容量&#xff0c;提升吞吐率&#xff0c;从理论上来看,多天线的空分复用能成倍增加系统容量。但实际上并非如此,如&#xff0c;22MIMO的容量C&#xff08;容量&…

深度学习实验3 - 卷积神经网络

文章目录实验要求数据集定义1 手写二维卷积1.1 自定义卷积通道1.2 自定义卷积层1.3 添加卷积层导模块中1.4 定义超参数1.5 初始化模型、损失函数、优化器1.6 定义模型训练和测试函数&#xff0c;输出训练集和测试集的损失和精确度1.7 训练1.8 loss及acc可视化2 torch.nn 实现二…