JUC并发编程第十篇,谈谈ThreadLocal原理与内存泄露的那些事儿

news2025/1/20 5:53:13

JUC并发编程第十篇,谈谈ThreadLocal原理与内存泄露的那些事儿

    • 一、ThreadLocal是什么?能干嘛?
    • 二、ThreadLocal 使用场景举例
    • 三、阿里开发规范中 ThreadLocal 的使用(SimpleDateFormat)
    • 四、ThreadLocal 底层源码架构分析
    • 五、ThreadLocal中的内存泄露问题
    • 六、ThreadLocal总结

一、ThreadLocal是什么?能干嘛?

  • ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问 ThreadLocal 实例的时候都有自己的、独立初始化的变量副本。
  • ThreadLocal 实例通常是类中的私有静态字段,使用它的目的是希望将状态与线程关联起来。
    在这里插入图片描述
    它实现了让每一个线程都有自己专属的本地变量副本,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

二、ThreadLocal 使用场景举例

  • 案例一:三个售票员卖完50张票,总量完成即可,售票员每个月固定月薪。
/**
 * 三个售票员卖完50张票,总量完成即可,售票员每个月固定月薪
 */
public class ThreadLocalDemo {
    public static void main(String[] args) {
        MovieTicket movieTicket = new MovieTicket();

        for (int i = 1; i <= 3; i++) {
            new Thread(() -> {
                for (int j = 0; j < 20; j++) {
                    movieTicket.saleTicket();
                    try { TimeUnit.MILLISECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
                }
            },String.valueOf(i)).start();
        }
    }
}

class MovieTicket{
    int number = 50;

    public synchronized void saleTicket(){
        if (number > 0){
            System.out.println(Thread.currentThread().getName()+"\t"+"号售票员卖出第: "+(number--));
        }else{
            System.out.println("--------------卖完了");
        }
    }
}
  • 案例二:比如房屋销售,要求员工根据自己的能力卖房,各自统计各自的,根据卖出的数量工资提成。
public class ThreadLocalDemo2 {
    public static void main(String[] args) {
        House house = new House();

        new Thread(() ->{
            try {
                for (int i = 1; i <= 3; i++) {
                    house.saleHouse();
                }
                System.out.println(Thread.currentThread().getName()+"\t"+"---"+house.threadLocal.get());
            } finally {
                //记得remove,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题
                house.threadLocal.remove();
            }

        },"t1").start();

        new Thread(() ->{
            try {
                for (int i = 1; i <= 8; i++) {
                    house.saleHouse();
                }
                System.out.println(Thread.currentThread().getName()+"\t"+"---"+house.threadLocal.get());
            } finally {
                //记得remove,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题
                house.threadLocal.remove();
            }

        },"t2").start();

        new Thread(() ->{
            try {
                for (int i = 1; i <= 12; i++) {
                    house.saleHouse();
                }
                System.out.println(Thread.currentThread().getName()+"\t"+"---"+house.threadLocal.get());
            } finally {
                //记得remove,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题
                house.threadLocal.remove();
            }

        },"t3").start();

        System.out.println(Thread.currentThread().getName()+"\t"+"---"+house.threadLocal.get());
    }
}

class House{

    ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

    public void saleHouse(){
        Integer value = threadLocal.get();
        value++;
        threadLocal.set(value);
    }
}

要记得remove(),如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题

案例总结:

  • 想要做到线程不争抢,有两种方式,第一:加 synchronized 或者 Lock 控制资源的访问顺序(效率低);第二:每个线程拥有自己独立的值(ThreadLocal)
  • 每个 Thread 内有自己的实例副本且该副本只由当前线程自己使用,ThreadLocal 统一设置初始值,但是每个线程对这个值的修改是各自线程互相独立的,既然其它 Thread 不可访问,那就不存在多线程间共享的问题。

三、阿里开发规范中 ThreadLocal 的使用(SimpleDateFormat)

在这里插入图片描述

  • SimpleDateFormat 是非线程安全的,官方建议为每个线程创建独立的格式实例,如果多个线程同时访问一个格式,则它必须保持外部同步。

并发环境下使用 SimpleDateFormat 的 parse 方法将字符串转换成 Date 对象,使用静态的成员变量是非常不安全的。

  • 代码演示:
public class DateUtils {
    public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static Date parseDate(String stringDate) throws Exception {
        return sdf.parse(stringDate);
    }

    public static void main(String[] args) {
        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                try {
                    System.out.println(DateUtils.parseDate("2022-12-12 11:11:11"));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

在这里插入图片描述
在这里插入图片描述

  • 这是为什么呢?

SimpleDateFormat 类内部有一个Calendar对象引用,它用来储存和这个 SimpleDateFormat 相关的日期信息,

如果你的 SimpleDateFormat 是个 static 的,那么多个 thread 之间就会共享这个 SimpleDateFormat,同时也是共享这个Calendar引用。

  • 如何解决?

方法一:将SimpleDateFormat定义成局部变量。

public class DateUtils {
    public static void main(String[] args) {
        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                try {
                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                    System.out.println(sdf.parse("2020-11-11 11:11:11"));
                    sdf = null;
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

缺点:每调用一次方法就会创建一个SimpleDateFormat对象,方法结束又要作为垃圾回收。

方法二:ThreadLocal,线程本地变量或者线程本地存储

/**
 * ThreadLocal可以确保每个线程都可以得到各自单独的一个SimpleDateFormat的对象,那么自然也就不存在竞争问题了。
 */
public class DateUtils2 {

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

    public static Date parseDateTL(String stringDate) throws ParseException {
        return sdf_threadLocal.get().parse(stringDate);
    }

    public static void main(String[] args) {
        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                try {
                    System.out.println(DateUtils2.parseDateTL("2022-12-12 11:11:11"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

方法三:DateTimeFormatter 代替 SimpleDateFormat

public class DateUtils2 {

    public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    public static String format(LocalDateTime localDateTime){
        return DATE_TIME_FORMATTER.format(localDateTime);
    }

    public static LocalDateTime parse(String dateString){
        return LocalDateTime.parse(dateString,DATE_TIME_FORMATTER);
    }

    public static void main(String[] args) {
        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                    System.out.println(DateUtils2.parse("2022-12-12 11:11:11"));
            },String.valueOf(i)).start();
        }
    }
}

四、ThreadLocal 底层源码架构分析

  • 从源码进行分析,Thread 类里边有一个 ThreadLocal
    在这里插入图片描述
  • ThreadLocal 里边有一个 ThreadLocalMap
  • ThreadLocalMap 里边实际干活的是一个 Entry
    在这里插入图片描述
  • 所以,Thread、ThreadLocal、ThreadLocalMap 组织关系总结如下:
    在这里插入图片描述

threadLocalMap 实际上就是一个以 threadLocal 实例为 key,任意对象为 value 的 Entry 对象。

ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map,不过它进行了两层包装,JVM内部维护了一个线程版的Map<Thread,T>,通过 ThreadLocal 对象的 set 方法,把 ThreadLocal 对象自己当做 key,放进了 ThreadLoalMap 中,每个线程要用到这个T的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量,保证并发模式下的安全。

五、ThreadLocal中的内存泄露问题

内存泄露:指不再会被使用的对象或者变量占用的内存不能被回收。

内存泄露是如何造成的呢?这里我们先要了解什么是强、软、弱、虚四个引用?

  • 强引用:最常见的普通对象引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用,当一个对象被强引用变量引用时,它处于可达状态,是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到,即使OOM,JVM也不会回收。
  • 软引用:通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收。
  • 弱引用:只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。
  • 虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列 (ReferenceQueue)联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态,在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。
假如有一个应用需要读取大量的本地图片:
 
	如果每次读取图片都从硬盘读取则会严重影响性能,
	如果一次性全部加载到内存中又可能造成内存溢出。
 
此时使用软引用可以解决这个问题:
 
  用一个HashMap来保存  图片的路径  和  相应图片对象关联的软引用  之间的映射关系,在内存不足时,
JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。

然后,回想刚才在上边看到的 ThreadLocalMap 源码,这个Map里边保存的是以 ThreadLocal 为 key 的对象,这个对象经过了两层包装:第一层使用 WeakReference 将 ThreadLocal 对象变成一个弱引用的对象,第二层是 定义了一个专门的类 Entry 来扩展 WeakReference。

这里为什么要使用弱引用呢?
在这里插入图片描述

  • 举个例子:
    • 当 func1 方法执行完后,栈帧销毁强引用 tl 也就没有了。但此时线程的 ThreadLocalMap 里某个 entry 的 key 引用还指向这个对象,如果这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏;
      如果使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null。

使用弱引用就不会出问题了吗?这里还存在一个问题

  • Entry中的key是弱引用,当 threadLocal 外部强引用被置为null(tl=null)时,那么系统 GC 的时候,根据可达性分析,这个threadLocal 实例就没有任何一条链路能够引用到它,这个ThreadLocal 就会被回收,这样一来,ThreadLocalMap 中就会出现一个 key 为 null 的 Entry。
  • 这样一来,我们就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前线程再迟迟不结束的话,这些 key 为 null 的Entry 的 value 就会迟迟无法回收,造成内存泄漏。
  • 只有当前 thread 运行结束,threadLocal,threadLocalMap,Entry没有引用链可达时,在垃圾回收的时候才都会被系统进行回收。
  • 但是,在使用线程池的情况下,为了复用,我们是不会结束线程的,就会造成内存泄漏。

总结:弱引用不能百分百保证内存不泄露,我们要在不使用某个ThreadLocal对象后,手动调用remove() 方法来删除它。

六、ThreadLocal总结

1、ThreadLocal 不是解决线程间共享数据问题的,而是用于 变量在线程间隔离且在方法间共享的场景。

2、它隐式的在不同线程内创建独立实例副本,避免了实例线程的安全问题。

3、每个线程持有一个只属于自己的Map,维护了ThreadLocal对象与具体实例的映射,该Map只能被持有它的线程访问,故不存在线程安全以及锁的问题。

4、ThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用,避免了 ThreadLocal 对象无法被回收的问题。

5、通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry 这三个方法回收键为 null 的 Entry 对象,从而防止内存泄漏。

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

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

相关文章

一个帖子教你实现 商品下单、获取商品订单信息、商品送货时发送通知 一体化功能

1、介绍 总览 通过无缝数据库结构&#xff0c;在保证数据的可用性、一致性、安全性外&#xff0c;云数据库能够实现数据在客户端和云端之间的无缝同步。云函数提供serverless环境&#xff0c;AppGallery Connect的其他服务为云函数提供事件源。监听事件源可以触发相关函数。 …

Word处理控件Aspose.Words功能演示:使用 C++ 将 PowerPoint 演示文稿转换为 Word 文档

Aspose API 支持旗下产品覆盖文档、图表、PDF、条码、OCR、CAD、HTML、电子邮件等各个文档管理领域&#xff0c;为全球.NET 、Java、C 等10余种平台开发人员提供丰富的开发选择。 在某些情况下&#xff0c;您有一个 PowerPoint 演示文稿并想要创建一个详细描述其内容的文档。为…

[附源码]Python计算机毕业设计电影院购票系统Django(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等…

小米手机系统好牛,真是国产系统的佼佼者

在十多年前智能手机&#xff0c;基本就没有定制、优化系统的概念&#xff0c;手机的图标都巨丑无比&#xff0c;而且还有好多不root都卸载不掉。当时买了一部漂亮国的某想手机&#xff0c;光预装应用就占了94%的空间&#xff0c;内容倒是特别“全面”&#xff0c;什么看小说的、…

“1只老母鸡”估值180亿!老乡鸡发展20年,都做对了什么?

酝酿了两年之久后&#xff0c;合肥终于迎来了养鸡IPO&#xff0c;估值180亿。 发迹于安徽、对标肯德基、麦当劳的老乡鸡餐饮股份有限公司更新了招股书&#xff0c;再度冲刺“中式快餐第一股”。 老乡鸡脱胎于2013年成立的“肥西老母鸡”&#xff0c;虽然已经走过了近二十个年头…

react源码中的生命周期和事件系统

这一章我想跟大家探讨的是React的生命周期与事件系统。 jsx的编译结果 因为前面也讲到jsx在v17中的编译结果&#xff0c;除了标签名&#xff0c;其他的挂在标签上的属性&#xff08;比如class&#xff09;&#xff0c;事件&#xff08;比如click事件&#xff09;&#xff0c;都…

《Python多人游戏项目实战》第一节 简单的方块移动

目录 1.1 设置游戏窗口 1.2 绘制一个方块 1.3 编写服务端代码 1.4 完善客户端代码 1.5 完整代码下载地址 在本节&#xff0c;我们将通过一个简单的方块移动程序进入多人联机游戏的大门。每个玩家打开游戏窗口后都可以控制一个方块&#xff0c;当某个玩移动方块后&#xff…

面试八股-Java并发

1.线程 和进程区别&#xff1a; 进程&#xff1a;独立地址空间 代码、数据、堆栈 资源分配基本单位线程&#xff1a;共享地址空间 线程ID、指令指针、寄存器集合和堆栈 调度分派基本单位 1.1.使用 实现Runable接口&#xff0c;run方法为实际逻辑实现Callable接口&#xff0…

离散数学与组合数学-数理逻辑-02谓词演算及其形式系统

文章目录第二章 谓词演算及其形式系统2.1 个体谓词和量词2.1.1 个体谓词演算永真式谓词公式的前束范式一阶谓词演算形式系统谓词逻辑的等值演算与推理第二章 谓词演算及其形式系统 2.1 个体谓词和量词 2.1.1 个体 个体常元(constants):确定的个体用a,b,ca,b,ca,b,c等小写字母…

羟基聚乙二醇叠氮 HO-PEG-N3/Azide的结构式

羟基聚乙二醇叠氮(HO-PEG-N3)是异双功能PEG衍生物之一。叠氮化物在铜离子催化的水溶液中与炔基有效反应。炔烃和叠氮化物之间的1,3-偶极环加成反应是一种高产率的点击化学反应&#xff0c;可实现两个相应分子的高效结合。叠氮化物也可以与应变促进的环辛炔反应&#xff0c;不需…

Pr:使用作品

利用作品 Production&#xff0c;可将大型复杂工作流拆分为多个可管理的 Pr 项目&#xff0c;并可在作品内跨项目、跨平台&#xff08;macOS 和 Windows&#xff09;引用媒体&#xff0c;无论这些资源是在本地存储或是共享的网络存储&#xff0c;作品可让一切保持同步。作品&am…

真希望你也明白runtime.Map和sync.Map

Map 官方介绍 One of the most useful data structures in computer science is the hash table. Many hash table implementations exist with varying properties, but in general they offer fast lookups, adds, and deletes. Go provides a built-in map type that imple…

Grafana监控大屏配置参数介绍(二)

Grafana 系列文章&#xff0c;版本&#xff1a;OOS v9.3.1 Grafana 的介绍和安装Grafana监控大屏配置参数介绍&#xff08;一&#xff09;Grafana监控大屏配置参数介绍&#xff08;二&#xff09; 上一篇文章已经介绍了图表可视化配置部分的 Panel options、Tooltip、Legend 3类…

音视频行业大势如何,优势在哪?

电信行业的变革&#xff1a; 从1G语音、2G短信、3G图片语音、4G视频到5G未来可期的新时代&#xff0c;见证了音视频行业的磅礴发展。 技术更新慢且门槛高 技术更新慢&#xff0c;技术门槛高&#xff0c;大部分技术沿用至今&#xff0c;依然保持生命力&#xff0c;技术人员成型…

http协议和websocket协议

http协议 HTTP 即超文本传输协议&#xff0c;是一种获取网络资源 (例如图像、HTML 文档) 的应用层协议&#xff0c;它是互联网数据通信的基础&#xff0c;由请求和响应构成。通常&#xff0c;首先客户端会发送 HTTP 请求(在请求报文中会指定资源的 URL)&#xff0c;然后用传输…

DocArray 0.20.0 发布!新增 Milvus 后端支持,更好地嵌套数据搜索,新增 RGB-D 格式的 3D 模型表示...

DocArray 是一个用于处理、传输和存储多模态数据的 Python 工具包。DocArray 提供便捷的多模态数据处理功能&#xff0c;具备基于 Protobuf 提供高性能的网络传输性能&#xff0c;同时也为多种向量存储方案提供统一的 API 接口。GitHub&#xff1a;github.com/docarray/docarra…

AU如何为你的人声增加空旷感?

你知道怎么使用AU给你的声音添加延迟效果&#xff0c;让你的声音具有空旷感和弱回声的效果。在这里我们可以使用插件达到这个目的。 在使用模拟延迟插件之前呢&#xff0c;我们可以去创建一个立体声总音轨&#xff0c;创建方式如图&#xff0c;跟着序号走&#xff0c;我们就可以…

CSS -- 06. CSS高阶技巧总结

文章目录CSS高阶技巧1 精灵图(sprites)1.1 为什么使用精灵图1.2 精灵图的使用2 字体图标2.1 字体图标的产生2.2 字体图标的优点2.3 字体图标的下载2.4 字体图标的引入2.5 字体图标的追加3 CSS三角形4 CSS用户界面样式4.1 鼠标样式 cursor4.2 表单的轮廓线4.3 防止拖拽文本域 re…

JAVA毕业设计——基于Springboot+vue的心理咨询管理系统(源代码+数据库)

github代码地址 https://github.com/ynwynw/psychlolgyhealth-public 毕业设计所有选题地址 https://github.com/ynwynw/allProject 基于Springbootvue的心理咨询管理系统(源代码数据库) 一、系统介绍 本项目分为管理员与普通用户两种角色 管理员角色包含以下功能&#xff…

[附源码]Python计算机毕业设计SSM基于Web美食网站设计(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…