Java 入门指南:Java 并发编程 —— 线程隔离技术 ThreadLocal

news2024/12/23 18:32:26

线程隔离技术

线程隔离是一种多线程编程技术,它可以将数据或资源在不同线程之间进行隔离,保证每个线程使用的数据或资源是独立的,不会互相干扰。线程隔离通常应用于高并发场景下,可以有效提升系统性能并提高并发能力。

实现方式

线程隔离的实现方式通常有以下几种:

  1. 每个线程使用自己的拷贝:每个线程单独维护一份数据或资源的拷贝,不与其他线程共享,从而实现隔离。

  2. 每个线程使用自己的命名空间:每个线程使用自己的命名空间,通过命名空间来隔离数据或资源,从而实现隔离。

  3. 线程局部变量(thread-local variable):线程局部变量是一种特殊的变量,在每个线程中都有自己的副本,不会与其他线程共享,从而实现隔离。Java 中提供了 ThreadLocal 类来实现线程局部变量。

缺陷

线程隔离虽然能够有效保证每个线程使用的数据或资源是独立的,但同时也会带来一些问题:

  1. 内存消耗增大:每个线程需要使用独立的数据或资源拷贝,因此会带来额外的内存消耗。

  2. 数据一致性问题:由于线程之间相互隔离,因此可能会引起数据一致性问题。

  3. 程序复杂度增加:线程隔离需要对数据或资源进行额外的管理和维护,程序复杂度可能会增加。

线程隔离与同步技术

线程隔离同步 是两种不同的技术,它们解决的问题和应用场景也有所不同:

  • 线程隔离主要关注的是隔离数据或资源,确保每个线程操作的是独立的数据或资源,从而提高系统的性能。

  • 同步主要关注的是多线程并发访问共享数据时的线程安全性,避免出现数据竞争和数据不一致的情况。

在某些情况下,线程隔离和同步可以结合使用,以确保在并发环境下既能保证数据隔离又能保证数据的一致性和安全性。例如,可以使用线程局部变量ThreadLocal 实现线程隔离,再结合使用同步机制,如 synchronized 或 ReentrantLock 锁,来保证对共享数据的同步访问。

线程本地存储

线程本地存储(Thread Local Storage,TLS)是一种机制,允许每个线程在使用时维护自己的私有数据副本。它提供了一种有效的方式来在多线程环境下封装线程特定的数据,使得每个线程都可以独立地访问和修改其自己的数据副本,而不会受到其他线程的干扰。

线程安全问题的核心在于多个线程会对同一个临界区的共享资源进行访问,如果每个线程都拥有自己的“共享资源”,独立操作自身的数据,互不影响,避免共享资源的竞争,就可以防止线程安全问题了。

在Java中,线程本地存储是通过 ThreadLocal 类来实现的。

ThreadLocal 是一个线程局部变量,它为每个线程提供了一个独立的变量副本。每个线程可以通过 get() 方法获取变量值,通过 set() 方法设置变量值。每个线程对该变量的操作都只会影响到自己的变量副本,不会影响其他线程的副本。

ThreadLocal

ThreadLocal 是 Java 中的一个线程本地存储类,位于 java.lang 包中。它的主要作用是提供一个线程的本地变量,每个线程拥有该变量独立的一个对象副本,每个线程都可以访问到自己线程的变量副本,保证线程之间的数据隔离性,避免共享资源的竞争。

这是一种“空间换时间”的思想,每个线程拥有自己的“共享资源”,虽然内存占用变大了,但由于不需要同步,也就减少了线程可能存在的阻塞问题,从而提高时间上的效率。

应用场景

ThreadLocal 对象通常用于在多线程环境下保持某些对象的状态,被应用最多的场景是 Session 管理 和 Connection 数据库链接管理,以避免线程安全问题。

  • 用于保存用户登录信息,这样在同一个线程中的任何地方都可以获取到登录信息。

  • 用于保存数据库连接、Session 对象等,这样在同一个线程中的任何地方都可以获取到数据库连接、Session 对象等。

  • 用于保存事务上下文,这样在同一个线程中的任何地方都可以获取到事务上下文。

  • 用于保存线程中的变量,这样在同一个线程中的任何地方都可以获取到线程中的变量。

常用方法

ThreadLocal 类中的常用方法:

  1. get(): 该方法用于获取与当前线程关联的 ThreadLocal 变量的值。如果当前线程还没有在线程本地存储中设置值,则返回 null

    当调用 get() 方法时,如果当前线程还没有在线程本地存储中设置值,则会调用 setinitialValue() 方法以懒初始化的方式来为该线程本地变量创建一个初始值(默认为 null),然后将该值保存在线程本地存储中,仅在实际需要特定于线程的值时才创建这些值。

  2. set(T value): 该方法用于将给定的值设置为当前线程的 ThreadLocal 变量的值。确保了每个线程都有自己的变量副本。

  3. remove(): 该方法用于从当前线程的线程本地存储中删除与线程本地变量相关联的值。等价于将线程本地变量设置为 null

  4. initialValue(): 该方法是一个 protected 方法,用于创建线程本地变量的初始值(默认为 null),ThreadLocal 子类可以重写该方法根据需要选取合适的初始值。

  5. withInitial(Supplier<? extends T> supplier): 该方法是 Java 8 新增的,它允许以更简单的方式创建线程本地变量,并为每个线程提供一个初始值,适用于 [[Java Lambda|Lambda]] 表达式。

当在一个线程中调用 ThreadLocalset 方法设置值时,该值只在当前线程内可见;当另一个线程调用 ThreadLocalset 方法设置同一个 ThreadLocal 的值时,它只会修改自己线程内的值,不会影响其他线程的值。

尽管 ThreadLocal 是线程局部变量,但如果没有清理,它们可能会导致内存泄漏或资源泄漏。当线程执行结束后,需要使用 remove() 方法将 ThreadLocal 变量与线程解绑并清理内存。

使用 ThreadLocal 可以解决一些多线程并发访问共享变量的线程安全问题,并提高程序的并发执行效率。但也应该避免过度使用 ThreadLocal,以免占用过多的内存。

使用步骤

以下是使用 ThreadLocal 的一般步骤:

  1. 创建一个 ThreadLocal 对象:ThreadLocal<MyObject>,其中MyObject 是要存储的数据类型。

  2. 在需要访问变量的线程中,通过 get() 方法获取ThreadLocal变量的值。

  3. 如果需要,可以通过 set() 方法将变量的值设置为当前线程的私有副本。

  4. 在线程结束时,记得通过 remove() 方法清理线程的 ThreadLocal 变量,以防止内存泄漏。

线程本地存储非常有用,特别是在一些需要在每个线程中保持上下文、状态或跟踪信息的情况下。例如,在 Web 应用程序中,可以使用线程本地存储来保存用户会话信息,以避免并发访问的问题。

ThreadLocal 示例

下面是一个示例,展示了如何使用 ThreadLocal 来存储和使用一个对象:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadLocalExample {

    // 创建一个 ThreadLocal 变量,存储 User 对象
    private static final ThreadLocal<User> threadLocal = new ThreadLocal<>();

    // 定义 User 类
    private static class User {
        private String name;
        private int age;

        public User(String name, int age) {
            this.name = name;
            this.age = age;
        }

        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        // 提交多个任务到线程池
        for (int i = 0; i < 5; i++) {
            final int index = i;
            executorService.submit(() -> {
                // 设置线程局部变量的值
                User user = new User("User " + index, 20 + index);
                threadLocal.set(user);

                // 在线程中使用该变量
                System.out.println("Thread " + Thread.currentThread().getId() + ": " + threadLocal.get());
            });
        }

        // 关闭线程池
        executorService.shutdown();
    }
}

示例说明:

  • 创建 ThreadLocal 变量threadLocal 是一个 ThreadLocal 对象,用于存储 User 类型的线程局部变量。

  • 设置和获取变量值:在每个线程中,使用 threadLocal.set(user) 设置线程局部变量的值。使用 threadLocal.get() 获取当前线程的局部变量值。

  • User 类:定义了一个简单的 User 类,用于存储用户的姓名和年龄。

  • 输出结果:每个线程都会输出自己的局部变量值,可以看到每个线程都有自己独立的 User 对象副本。

ThreadLocalMap

ThreadLocalMap 是 Java 中的一个类,用于实现 ThreadLocal 类的底层数据结构,是 ThreadLocal 的核心。

每个线程都有一个对应的 ThreadLocalMap 对象,它通过 Thread.threadLocals 字段来引用。因此,每个线程可以独立地操作自己的 ThreadLocalMap 对象。不会与其他线程的 ThreadLocal 变量产生冲突。

ThreadLocalMap 是一个哈希桶数组,通过哈希表实现,每个桶都存储一个 Entry 对象,Entry 对象包含了 ThreadLocal 的引用和对应的值。ThreadLocalMap 实际上就是一个以 ThreadLocal 实例为 key,任意对象为 value 的 Entry 数组

在使用 ThreadLocal 类时,通常会通过 ThreadLocalMap 来实现线程本地变量的存储。为 ThreadLocal 变量赋值时,实际上就是以当前 ThreadLocal 实例为 key,值为 Entry 向其 ThreadLocalMap 中存放。每个线程可以独立地访问和操作自己的线程本地变量,

Entry 对象

![[ThreadLocalMap Entry Source Code.png]]

ThreadLocalMap 中的键值对是弱引用关联的,这意味着当一个 ThreadLocal 对象的强引用被释放后,对应的键值对可能会被垃圾回收。

内存泄漏问题

ThreadThreadLocalThreadLocalMapEntry 的关系,实线表示强引用,虚线表示弱引用

![[ThreadLocal Model.png]]
ThreadLocal 外部强引用被置为 null(ThreadLocalInstance=null )时,根据可达性分析,ThreadLocal 实例此时没有任何一条链路引用它,所以系统 GC 的时候 ThreadLocal 会被回收。

实际开发中,线程为了复用是不会主动结束的,由于线程迟迟不结束,这些 key 为 null 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,无法回收就会造成内存泄漏。因此像数据库连接池这样过大的线程池可能会增加内存泄漏的风险。

为了避免这个问题,在每次使用完 ThreadLocal 之后,最好明确调用 ThreadLocal 的 remove 方法来删除与当前线程关联的值。这样可以确保线程再次使用时不会存储旧的、不再需要的值。

ThreadLocal 的 hashCode

ThreadLocalhashCode 是通过 nextHashCode() 方法获取的,该方法实际上是用 Atmoic 包的 AtomicInteger 加上 0x61c88647 来实现的。

0x61c88647 是一个魔数,用于 ThreadLocal 的哈希码递增。这个值的选择并不是随机的,是一个特定的质数,具有以下特性:

  • 质数:它是一个质数,这意味着它不能被除 1 和它本身之外的任何数字整除。

  • 黄金比例:这个数字大约等于黄金比例的 32 位浮点表示的一半。黄金比例具有一些有趣的数学特性,其中之一是与斐波那契数列的关系。

  • 递增分布:每当创建新的 ThreadLocal 对象时,都会将此值添加到上一个 ThreadLocal 的哈希码中。这个递增的步长有助于在哈希表中均匀地分配 ThreadLocal 对象。

  • 性能优化:通过使用这个特定的值,算法能够确保哈希码的均匀分布,从而减少哈希冲突的可能性。

解决哈希冲突

由于 ThreadLocalMap 基于哈希表实现,那么在存储数据的过程中就可能出现哈希冲突,降低查找的效率。而 ThreadLocalMap 使用开放寻址法(open addressing),通过线性探测法(linear probing)来处理哈希冲突。

当一个桶已经被占用时,ThreadLocalMap 会尝试寻找下一个空闲的桶,一般是往后顺延搜索。这个过程会一直进行,当到哈希表末尾的时候再从 0 开始,循环,直到找到一个空闲的桶来存储新的键值对或者覆盖已有的键值对。

线性探测的开放寻址法也有一些潜在的问题。当哈希桶数组的装载因子(load factor)过高时,即桶中被占用的比例接近或超过阈值时,会导致哈希冲突的频率增加,进而影响性能。为了解决这个问题,ThreadLocalMap 在内部进行了自动扩容,以保证装载因子在一个合理的范围内,提高查找效率。

set 源码

![[ThreadHashMap Set.png]]

  • replaceStaleEntry:向 ThreadLocalMap 添加新数据时,可以检查是否有“脏” Entry(key 为 null 的 Entry),并用新的数据替换它

  • cleanSomeSlots:在某些操作过程中(例如添加、获取等),通过遍历哈希表,删除 key 为 null 的脏 Entry

扩容机制

HashMapThreadLocalMap 初始大小为 16,负载因子(哈希表中已经存放的条目数量与哈希表容量的比例)为 2 3 \frac{2}{3} 32,所以哈希表可用大小为: 16 × 2 3 = 10 16\times\frac{2}{3} = 10 16×32=10,即哈希表可用容量为 10。

当哈希表的 size 大于 threshold(临界值) 的时候,会通过 resize() 方法进行扩容。

新建一个数组,其大小为原来数组长度的两倍,然后遍历旧数组中的 Entry 并将其插入到新的数组中。在扩容的过程中,将脏 Entry 的 value 设为 null,以便被垃圾回收,解决隐藏的内存泄漏问题。移除脏 Entry后,重新确定 Entry 在新数组的位置,然后进行插入。最后,设置新哈希表的 threshholdsize 属性。

remove() 方法

![[ThreadLocal remove().png]]

  1. 通过局部变量 tab 获取 ThreadLocalMap 的哈希表数组,并获取数组的长度。

  2. 通过 key.ThreadLocalHashCode & (len-1) 计算给定ThreadLocal 键的哈希索引。确定索引位置。

  3. 使用开放寻址法遍历哈希表,通过 nextIndex(i, len) 计算下一个索引以处理哈希冲突。

  4. 如果找到与给定键匹配的条目(e.get() == key),执行以下操作:

    • 清除键:通过调用 e.clear() 方法,将条目的键置为 null。由于 Entry 是 WeakReference 的子类,clear 方法将断开对ThreadLocal 对象的引用,允许垃圾收集器在需要时回收它。

    • 清除值:通过调用 expungeStaleEntry(i) 方法,清除该条目的值并对哈希表进行部分清理。该方法的目的是清除哈希表中的无效元素,即那些其键已被垃圾收集的元素。

  5. 结束删除操作:一旦找到并删除了匹配的条目,方法返回。如果遍历整个哈希表都没有找到匹配的键,则该方法不执行任何操作并正常返回。

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

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

相关文章

MySQL record 03 part

插入表数据&#xff1a; 一般情况下&#xff0c;向表中添加新的记录&#xff0c;应该包含此表的所有字段&#xff0c;也就是应该给表的所有字段添加值&#xff0c; 1.使用insert into语句&#xff0c;指定字段名&#xff08;可以是所有的字段&#xff0c;也可以是某几个字段&am…

Android Framework(四)WMS-窗口显示流程——窗口创建与添加

文章目录 流程概览涉及模块流程概览 应用端——window创建&#xff1a;Activity::attach创建window流程setWindowManager&#xff0c;getWindowManagerDecorView 应用端——window的显示流程&#xff1a;Activity::onResumeViewRootImpl::setViewmWindowSession 是什么mWindow是…

【数据库】MySQL聚合统计

目录 1.聚合函数 案例1&#xff1a; 统计班级共有多少同学 案例2&#xff1a;统计本次考试的数学成绩分数个数 案例3&#xff1a;统计数学成绩总分 案例4&#xff1a;统计平均总分 案例5&#xff1a;返回英语最高分 案例6&#xff1a;返回 > 70 分以上的数学最低分 2.分…

双指针思想

一.双指针思想 1.分类&#xff1a;同向双指针&#xff0c;反向双指针 2.优点&#xff1a;可以将两层循环嵌套的问题优化成一层循环 3.常见情况 <1>利用快慢双指针确定链表的中间节点&#xff0c;链表是否带环&#xff0c;带环链表的入环点在哪里 <2>一次循环解…

Android Fragment 学习备忘

1.fragment的动态添加与管理&#xff0c;fragment生命周期在后面小节&#xff1a;https://www.bilibili.com/video/BV1Ng411K7YP/?p37&share_sourcecopy_web&vd_source982a7a7c05972157e8972c41b546f9e4https://www.bilibili.com/video/BV1Ng411K7YP/?p37&share_…

安装Android Studio及第一个Android工程可能遇到的问题,gradle下载过慢、sync失败?

Android Studio版本众多&#xff0c;电脑操作系统、电脑型号、电脑硬件也是多种多样&#xff0c;幸运的半个小时内可以完成安装&#xff0c;碰到不兼容的电脑&#xff0c;一天甚至更长时间都无法安装成功。 Android安装及第一个Android工程分为4个步骤&#xff0c;为什么放到一…

9.8笔试记录

1.在c中哪些运算符不能重载? 在 C 中&#xff0c;有以下几个运算符不能被重载&#xff1a; . &#xff1a;成员访问运算符。例如obj.member中的.不能被重载。 :: &#xff1a;作用域解析运算符。用于指定命名空间、类等的作用域&#xff0c;不能被重载。 ?: &#xff1…

spring揭秘19-spring事务01-事务抽象

文章目录 【README】【1】事务基本元素【1.1】事务分类 【2】java事务管理【2.1】基于java的局部事务管理【2.2】基于java的分布式事务管理【2.2.1】基于JTA的分布式事务管理【2.2.2】基于JCA的分布式事务管理 【2.3】java事务管理的问题 【3】spring事务抽象概述【3.1】spring…

easyExcel-读取Excel

1、简单读取&#xff0c;没有合并单元格 2、复杂读取&#xff0c;合并单元格-方法一 1、简单读取&#xff0c;没有合并单元格 1.1、引入pom文件 <dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.…

【单片机】详细解析完全重映射和部分重映射

1、重映射的作用 单片机中的每个引脚通常都有多个功能&#xff0c;不仅仅是作为普通的输入或输出&#xff0c;还可以与某些外设&#xff08;如定时器、串口、SPI、I2C等&#xff09;关联。默认情况下&#xff0c;这些外设功能通常固定绑定到特定的GPIO引脚。但是&#xff0c;在…

Matplotlib颜色透明度设置

matplotlib中的透明度设置都是通过alpha设置的,一般在能设置颜色的包括背景色、图表色、文字颜色都可以设置透明度 float类型,取值范围为[0.0,1.0],alpha取值越小越透明 import numpy as np import matplotlib.pyplot as pltx np.linspace(0, 2*np.pi, 100) y np.sin(x) y1…

Arch - 架构安全性_授权(Authorization)

文章目录 OverView授权&#xff08;Authorization&#xff09;RBAC&#xff1a; 概述RBAC&#xff1a;基于角色的访问控制RBAC&#xff1a;主要元素 OAuth2&#xff1a;面向第三方应用的认证授权协议业务场景OAuth2的工作流程OAuth2 四种不同的授权方式授权码模式&#xff08;A…

Android Studio 2024最新版Hello World

Android Studio 2024最新版Hello World 1. Android Studio 2024安装视频2. 创建项目Read Timed out 问题Android Studio Build Output 控制台中文乱码问题 3. 驱动管理 本文章介绍如何通过Android Studio 2024最新版创建项目&#xff0c; 并成功输出Hello World。 本次教程版本…

关于QT中使用网络编程QtNetwork的问题

在此处添加network模块 在链接器中添加附加库目录(QT对应的lib)在链接器-输入中添加对应的lib库(Qt5Network.lib) 这样,就可以使用对应的下面的库文件了,比如: #include <qnetworkaccessmanager.h>

参数高效微调(PEFT)综述

人工智能咨询培训老师叶梓 转载标明出处 大模型如BERT和GPT-3的参数数量庞大&#xff0c;动辄数十亿甚至数千亿级别&#xff0c;给进一步的微调和应用带来了巨大的挑战。针对这一问题&#xff0c;Vladislav Lialin、Vijeta Deshpande、Anna Rumshisky等研究者中提出了一系列参…

Leetcode面试经典150题-69.X的平方根

相当简单的题目&#xff0c;但是出现的概率还挺高的 解法都在代码里&#xff0c;不懂就留言或者私信 class Solution {public int mySqrt(int x) {/**0的平方根是0 */if(x 0) {return 0;}/**1~3的平方根是1 */if(x < 3) {return 1;}/**其他情况我们采用二分查找&#xff…

Python 调用手机摄像头

Python 调用手机摄像头 在手机上安装软件 这里以安卓手机作为演示&#xff0c;ISO也是差不多的 软件下载地址 注意&#xff1a;要想在电脑上查看手机摄像头拍摄的内容的在一个局域网里面(没有 WIFI 可以使用 热点 ) 安装完打开IP摄像头服务器 点击分享查看IP 查看局域网的I…

Android Studio下载Gradle失败问题解决

问题说明 使用 Android Studio 构建程序报错如下 Could not install Gradle distribution from https://services.gradle.org/distributions/gradle-7.5.1-bin.zip. Reason: java.net.SocketTimeoutException: Connect timed out问题解决 下载对应版本的压缩包 gradle-7.5.1…

香橙派转换模型以及在开发板上部署

新手小白记录一下自己使用香橙派模型转换以及在开发板上运行的过程&#xff0c;防止后面忘记。 使用的开发板&#xff1a;Orange Pi 5 Plus&#xff08;rk3588&#xff09; 官方的一些资料在&#xff08;主要参考用户手册&#xff09;&#xff1a;Orange Pi - Orangepihttp:/…

价格适中超微小间距P1.8全彩LED显示屏广泛应用于COB会议一体机

随着科技的不断进步与市场需求的日益多样化&#xff0c;价格适中且具备超微小间距P1.8技术的全彩LED显示屏&#xff0c;在COB&#xff08;Chip On Board&#xff09;会议一体机领域的应用正逐步深化&#xff0c;引领着现代会议展示的新风尚。这种显示屏不仅以其细腻的画质和卓越…