ThreadLocal和Synchronized的区别

目录

  • 背景
  • 过程
    • ThreadLocal
      • 什么是ThreadLocal?
      • 既然都是保证线程访问的安全性,那么和Synchronized区别是什么呢?
      • ThreadLocal的使用
      • TheadLocal使用场景
      • 原理
      • 高并发场景下ThreadLocal会造成内存泄漏吗?什么原因导致?如何避免?
      • 如何避免

背景

明确ThreadLocal和Synchronized 之间的区别

过程

ThreadLocal

什么是ThreadLocal?

ThreadLocal英文翻译过来就是:线程本地量,它其实是一种线程的隔离机制,保障了多线程环境下对于共享变量访问的安全性。
看到上面的定义之后,那么问题就来了,ThreadLocal是如何解决共享变量访问的安全性的呢?
其实ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。由于副本都归属于各自的线程,所以就不存在多线程共享的问题了。
便于理解,我们看一下下图。

在这里插入图片描述
至于上述图中提及的threadLocals(ThreadLocalMap),我们后文看源代码的时候再继续来看。大家心中暂时有个概念。

既然都是保证线程访问的安全性,那么和Synchronized区别是什么呢?

在上面聊到共享变量访问安全性的问题上,其实大家还会很容易想起另外一个关键字Synchronized。聊聊区别吧,整理了一张图,看起来可能会更加直观一些,如下。

在这里插入图片描述
通过上图,我们发现ThreadLocal其实是一种线程隔离机制。Synchronized则是一种基于Happens-Before规则里的监视器锁规则从而保证同一个时刻只有一个线程能够对共享变量进行更新。
Synchronized加锁会带来性能上的下降。ThreadLocal采用了空间换时间的设计思想,也就是说每个线程里面都有一个专门的容器来存储共享变量的副本信息,然后每个线程只对自己的变量副本做相对应的更新操作,这样避免了多线程锁竞争的开销。

ThreadLocal的使用

上面说了这么多,咱们来使用一下。就拿SimpleDateFormat来做个例子。当然也会有一道这样的面试题,SimpleDateFormat是否是线程安全的?在阿里Java开发规约中,有强制性地提到SimpleDateFormat 是线程不安全的类。其实主要的原因是由于多线程操作SimpleDateFormat中的Calendar对象引用,然后出现脏读导致的。

public class DateFormatTest {
    private static final SimpleDateFormat simpleDateFormat =
            new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static Date parse(String dateString) {
        Date date = null;
        try {
            date = simpleDateFormat.parse(dateString);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }

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

        for (int i = 0; i < 20; i++) {
            executorService.execute(()->{
                System.out.println(parse("2024-02-01 23:34:30"));
            });
        }
        executorService.shutdown();
    }
}

上述咱们通过线程池的方式针对SimpleDateFormat进行了测试

在这里插入图片描述
们通过ThreadLocal的方式将其优化一下。代码如下:

public class DateFormatTest {

    private static final ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static Date parse(String dateString) {
        Date date = null;
        try {
            date = dateFormatThreadLocal.get().parse(dateString);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }

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

        for (int i = 0; i < 20; i++) {
            executorService.execute(()->{
                System.out.println(parse("2024-02-01 23:34:30"));
            });
        }
        executorService.shutdown();
    }
}

TheadLocal使用场景

  1. 上面针对SimpleDateFormat的封装也算是一个吧。
  2. 用来替代参数链传递:在编写API接口时,可以将需要传递的参数放入ThreadLocal中,从而不需要在每个调用的方法上都显式地传递这些参数。这种方法虽然不如将参数封装为对象传递来得常见,但在某些情况下可以简化代码结构。
  3. 数据库连接和会话管理:在某些应用中,如Web应用程序,ThreadLocal可以用来保持对数据库连接或会话的管理,以简化并发控制并提高性能。例如,可以使用ThreadLocal来维护一个连接池,使得每个请求都能共享相同的连接,而不是每次都需要重新建立连接。
  4. 全局存储信息:例如在前后端分离的应用中,ThreadLocal可以用来在服务端维护用户的上下文信息或者一些配置信息,而不需要通过HTTP请求携带大量的用户信息。这样做可以在不改变原有架构的情况下,提供更好的用户体验。

项目使用:
在这里插入图片描述
在这里插入图片描述个代码实际上使用了单例模式,具体来说是使用了双重检查锁定(double-checked locking)的变种,也被称为静态内部类(static inner class)单例模式。这种实现方式利用了Java类加载机制的特性,在UserContextHolder类首次被加载时,会创建SingletonHolder类的一个实例,同时初始化其中的sInstance字段,这是线程安全的。

下面是关于代码中单例模式使用的详细解释:

UserContextHolder类有一个私有的构造器private UserContextHolder(),这是为了确保外部无法直接实例化UserContextHolder。

getInstance()方法用于获取UserContextHolder的实例。这个方法调用了UserContextHolder.SingletonHolder.sInstance,而sInstance是在SingletonHolder静态内部类中初始化的。

SingletonHolder是一个私有的静态内部类,它持有一个静态的UserContextHolder实例sInstance。由于Java的类加载机制保证了静态内部类只会在第一次被引用时加载,因此sInstance只会被初始化一次,并且这个过程是线程安全的。

所以,尽管代码中并没有直接显示诸如synchronized关键字或者显式的锁机制,但通过利用Java的类加载机制,这个实现已经保证了单例的创建是线程安全的。

此外,UserContextHolder类使用ThreadLocal来存储线程上下文。每个线程都有它自己的ThreadLocal变量副本,因此,setContext、getContext和clear方法可以在不同的线程中独立地设置、获取和清除它们的上下文数据,而不会影响到其他线程。

在这里插入图片描述

这样的话,每个请求过来的时候都在在过滤器那里解密,从而获取一些信息存储在ThreadLocal 变量中,然后在需要这些参数的时候直接拿出啦,使用,我认为这里既进行了全局存储信息,有代替参数链传递,而且这里还用了单例模式,

原理

在这里插入图片描述

图中有两个线程Thread1以及Thread2。
Thread类中有一个叫做threadLocals的成员变量,它是ThreadLocal.ThreadLocalMap类型的。
ThreadLocalMap内部维护了Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型对象值。

高并发场景下ThreadLocal会造成内存泄漏吗?什么原因导致?如何避免?

首先明确前提:

引用的强弱,稍微聊一下,这里其实涉及到jvm的回收机制。在JDK1.2之后,java对引用的概念其实做了扩充的,分为强引用,软引用,弱引用,虚引用。

强引用:其实就是咱们一般用“=”的赋值行为,如 Student s = new Student(),只要强引用还在,对象就不会被回收。

软引用:不是必须存活的对象,jvm在内存不够的情况下即将内存溢出前会对其进行回收。例如缓存。

弱引用:非必须存活的对象,引用关系比软引用还弱,无论内存够还是不够,下次的GC一定会被回收。

虚引用:别名幽灵引用或者幻影引用。等同于没有引用,唯一的目的是对象被回收的时候会受到系统通知。

明白这些概念之后,咱们再看看上面的源代码,我们就会发现,原来Key其实是弱引用,而里面的value因为是直接赋值行为所以是强引用。

在这里插入图片描述

图中我们可以看到由于threadLocal对象是弱引用,如果外部没有强引用指向的话,它就会被GC回收,那么这个时候导致Entry的key就为NULL,如果此时value外部也没有强引用指向的话,那么这个value就永远无法访问了,按道理也该被回收。但是由于entry还在强引用value(看源代码)。那么此时value就无法被回收,此时内存泄漏就出现了。本质原因是因为value成为了一个永远无法被访问也无法被回收的对象。

那肯定有小伙伴会有疑问了,线程本身生命周期不是很短么,如果短时间内被销毁,就不会内存泄漏了,因为只要线程销毁,那么value也会被回收。这话是没错。但是咱们的线程是计算机珍贵资源,为了避免重复创建线程带来开销,系统中我们往往会使用线程池,如果使用线程池的话,那么线程的生命周期就被拉长了,那么就可想而知了。

内存泄漏的原因:

1、长生命周期的 ThreadLocal:
如果 ThreadLocal 的实例被声明为静态的,那么它的生命周期将会很长,可能会与应用程序的生命周期一样长。

2、ThreadLocalMap 的引用:
当线程结束时,通常它的栈帧会被垃圾回收器回收。但是,如果 ThreadLocal 的实例还存在(因为它是静态的),那么它持有的 ThreadLocalMap 的引用就不会被释放。

3、Entry 的强引用:
在 ThreadLocalMap 中,键(ThreadLocal 的弱引用)和值(实际存储的对象)都是强引用。这意味着即使 ThreadLocal 的键被回收(因为它是弱引用),只要值还存在,Entry 对象就不会被回收。

4、内存泄漏的发生:
如果线程池中的线程被重复使用,而 ThreadLocal 没有在不再需要时被正确清理(即调用 remove() 方法),那么 ThreadLocalMap 中存储的值就会一直存在,即使这些值已经不再需要。随着时间的推移,这会导致内存泄漏,因为垃圾回收器无法回收这些不再使用的对象。

如何避免

每次使用完毕之后记得调用一下remove()方法清除数据。
ThreadLocal变量尽量定义成static final类型,避免频繁创建ThreadLocal实例。这样可以保证程序中一直存在ThreadLocal强引用,也能保证任何时候都能通过ThreadLocal的弱引用访问Entry的value值,从而进行清除。
不过话说回来,其实ThreadLocal内部也做了优化的。在set()的时候也会采样清理,扩容的时候也会检查(这里希望大家自己深入看一下源代码),在get()的时候,如果没有直接命中或者向后环形查找的时候也会进行清理。但是为了系统的稳健万无一失,所以大家尽量还是将上面的两个注意点在写代码的时候注意下。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

aws 入门篇 01.aws学习的方法论

aws入门篇 01.aws学习的方法论 第1章 aws学习的方法论 aws的服务很多&#xff0c;现在应该有100多个服务了&#xff0c;怎么来学习aws呢&#xff1f; 这几年也使用了一些aws的服务&#xff0c;谈谈自己对学习aws的理解。 1.先横向&#xff0c;后纵深 比如说&#xff0c;aws最…

SpringCloud微服务集成Dubbo

1、Dubbo介绍 Apache Dubbo 是一款易用、高性能的 WEB 和 RPC 框架,同时为构建企业级微服务提供服务发现、流量治理、可观测、认证鉴权等能力、工具与最佳实践。用于解决微服务架构下的服务治理与通信问题,官方提供了 Java、Golang 等多语言 SDK 实现。使用 Dubbo 开发的微服…

手撕算法-最小覆盖子串

描述 分析 滑动窗口。 参考力扣官方的题解思路 本问题要求我们返回字符串 s 中包含字符串 t 的全部字符的最小窗口。我们称包含 t 的全部字母的窗口为「可行」窗口。 我们可以用滑动窗口的思想解决这个问题。在滑动窗口类型的问题中都会有两个指针&#xff0c;一个用于「延伸…

文件操作(下)(想要了解如何操作文件,那么看这一片就足够了!)

前言&#xff1a;在文件操作&#xff08;上&#xff09;中&#xff0c;我们讲到了基础的文件操作&#xff0c;包括文件的打开&#xff0c;文件的关闭&#xff0c;以及文件的基础读写&#xff0c;那么除了之前学习的读写之外&#xff0c;还有什么其他的方式对文件进行读写操作吗…

P5725 【深基4.习8】求三角形

【深基4.习8】求三角形 - 洛谷https://www.luogu.com.cn/problem/P5725 import java.util.*;public class Main {public static void main(String[] args) {Scanner sc new Scanner(System.in); // 创建一个 Scanner 对象来读取用户输入int n sc.nextInt(); // 从用户输入中…

Linux根据时间删除文件或目录

《liunx根据时间删除文件》和 《Linux 根据时间删除文件或者目录》已经讲述了根据时间删除文件或目录的方法。 下面我做一些补充&#xff0c;讲述一个具体例子。以删除/home目录下的文件为例。 首先通过命令&#xff1a; ls -l --time-style"%Y-%m-%d %H:%M:%S"…

【数据结构与算法】快速排序(详解:快排的Hoare原版,挖坑法和双指针法|避免快排最坏时间复杂度的两种解决方案|小区间优化|非递归的快排)

引言 快速排序作为交换排序的一种&#xff0c;在排序界的影响力毋庸置疑&#xff0c;我们C语言中用的qsort&#xff0c;C中用的sort&#xff0c;底层的排序方式都是快速排序。相比于同为交换排序的冒泡&#xff0c;其效率和性能就要差的多了&#xff0c;本篇博客就是要重点介绍…

2024 ccfcsp认证打卡 2023 03 01 田地丈量

import java.util.Scanner;public class Main {public static void main(String[] args) {Scanner in new Scanner(System.in);int n in.nextInt(); // 输入 n&#xff0c;表示矩形的数量int a in.nextInt(); // 输入 a&#xff0c;表示整个区域的长度int b in.nextInt()…

3.28学习总结

java 封装 封装体现了java的面向对象的特点,用户不用知道程序是如何运行的,只用按照所给的格式输入参数,便可得到对应的结果. 一个完整的封装需要每个实例变量都用private来修饰并拥有相应的public getter和setter方法. 代码 public class girl {private int age;public st…

C++:转义符(10)

在c中有一些字符无法被显示出来&#xff0c;所以需要使用些特殊字符加字母来展示 可以看到基本都是一个\加一个字母去只执行对应的一个效果 这里我选择几个对于当前来说比较重要的&#xff1a;\n &#xff0c;\\ &#xff0c;\t \n换行符 可以看到在c语言中他就是一个可以换行…

c++ 有名对象和匿名对象

c 有名对象和匿名对象 有名对象就是有名字的对象&#xff0c;匿名对象就是没有名字的对象。 #define _CRT_SECURE_NO_WARNINGS 1 using namespace std; #include<iostream> class score { public:score(){math 100;chinese 100;english 100;}score(int _math, int _…

GitHub开源项目权限管理-使用账号和个人令牌访问

1.打开后台账号设置 2.找到左下角的Developer settings 3.找到Personal access tokens 的 Tokens(classic) 4.选择创建新证书 5.填写证书信息 6.点击生成证书&#xff0c;复制证书并且保存起来&#xff08;血泪教训&#xff0c;证书只会在创建时显示一次&#xff0c;以后就再也…

<QT基础(5)>事件监听

事件监听 事件监听&#xff08;Event Handling&#xff09;是在程序中监视和响应发生的事件的一种机制。在Qt中&#xff0c;事件监听是一种常见的用于处理用户输入、系统事件以及其他类型事件的方法。通过事件监听&#xff0c;您可以在发生特定事件时捕获事件并执行相应的操作…

Ainx的多路由模式

&#x1f4d5;作者简介&#xff1a; 过去日记&#xff0c;致力于Java、GoLang,Rust等多种编程语言&#xff0c;热爱技术&#xff0c;喜欢游戏的博主。 &#x1f4d7;本文收录于Ainx系列&#xff0c;大家有兴趣的可以看一看 &#x1f4d8;相关专栏Rust初阶教程、go语言基础系列…

使用Zabbix监控NAS目录状态

在企业的数据存储和共享中,网络附加存储(NAS)扮演着至关重要的角色。为了确保NAS设备的稳定运行和数据的完整性,对其进行实时监控是必不可少的。Zabbix作为一款开源的网络监控解决方案,能够帮助我们实现这一目标。本文将介绍如何使用Zabbix监控NAS目录状态,以确保及时发现…

如何在CentOS使用Docker搭建MinIO容器并实现无公网ip远程访问本地服务

文章目录 前言1. Docker 部署MinIO2. 本地访问MinIO3. Linux安装Cpolar4. 配置MinIO公网地址5. 远程访问MinIO管理界面6. 固定MinIO公网地址 前言 MinIO是一个开源的对象存储服务器&#xff0c;可以在各种环境中运行&#xff0c;例如本地、Docker容器、Kubernetes集群等。它兼…

MySQL数据库 - 复杂查询(一)

一个不知名大学生&#xff0c;江湖人称菜狗 original author: Jacky Li Email : 3435673055qq.com Time of completion&#xff1a;2024.03.27 Last edited: 2024.03.27 目录 MySQL数据库 - 复杂查询&#xff08;一&#xff09; 第1关&#xff1a;交换工资 任务描述 相关知…

电平输入检测-定时器输入捕获

目录 一&#xff0c;引入 二&#xff0c;具体结构 三&#xff0c;实现步骤 四&#xff0c;PWM输入模式 一&#xff0c;引入 上篇博客&#xff0c;我们对于定时器的计数核心——时基单元作了细致的了解。这篇博文&#xff0c;我们来介绍定时器的四大功能模块之一——输入捕获…

Python基本运算

1.逻辑运算符 第四行会有黄色的下划线是因为这个不是系统推荐的写法&#xff0c;系统推荐的是第五行的链式比较&#xff1b; 2.短路求值 对于and而言&#xff0c;左边的语句是false&#xff0c;那么整体一定是false,右边的表达式就不会进行计算&#xff1b; 对于or而言&…

ChatGLM3:AttributeError_ can‘t set attribute ‘eos_token‘

最近在微调 ChatGLM3-6b 时&#xff0c;训练好模型之后&#xff0c;调用inference_hf.py函数验证模型的时候报了如下错误&#xff0c;下面是解决方案。 我在训练时使用的是ptuning_v2.yaml配置文件&#xff0c;训练运行代码如下&#xff1a; CUDA_VISIBLE_DEVICES1 python fi…