热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

JAVA并发(4)—ThreadLocal源码角度分析是否真正能造成内存溢出!

内存泄漏和内存溢出: 内存泄漏是指程序在申请内存后,无法释放已申请的内存空间就会造成内存泄漏。一次内存泄漏似乎不会造成很大影响。但内存泄漏累积的效果就是内存溢出。 场景1:是否会造成内存溢出:

内存泄漏和内存溢出:

内存泄漏是指程序在申请内存后,无法释放已申请的内存空间就会造成内存泄漏。一次内存泄漏似乎不会造成很大影响。但内存泄漏累积的效果就是内存溢出。

场景1:是否会造成内存溢出:

场景描述:线程池中只有一个线程。每一次线程启动,均会初始化一个threadLocal对象。

该场景便是使用ThreadLocal的反面教材,即使用ThreadLocal但未使用remove()方法清除。

public class ThreadLocalDemo {
    private static Logger logger= LoggerFactory.getLogger(WeakReferenceDemo.class);
    private ThreadLocal threadLocal = new ThreadLocal();

    //开启一个线程池
    public static void testThreadLocalByPool(){
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        while (true) {
            executorService.execute(() -> {
                //每new出一个对象,那么便new出一个ThreadLocal
                ThreadLocalDemo demo = new ThreadLocalDemo();
                demo.testThreadLocal();
            });
        }
    }
    public  void testThreadLocal() {
        //ThreadLocal是方法级别的
        threadLocal.set("aaa");
        String s = threadLocal.get();
        logger.info("获取ThreadLocal的内容:"+s);
    }
    //测试ThreadLocal不会发生内存溢出
    public static void main(String[] args) {
        testThreadLocalByPool();
    }
}

结论:该代码最终也不会发生内存溢出。

实际上ThreadLocal仅仅会造成内存泄漏,若是存在大量线程的情况(蚂蚁咬死象),可能会造成内存溢出。

ThreadLocal的源码分析

ThreadLocal含义为线程本地变量。即每个线程中均存在一个ThreadLocal对象。

JAVA并发(4)— ThreadLocal源码角度分析是否真正能造成内存溢出!
图1-ThreadLocal与线程的关系.png

1.1 ThreadLocalMap的set方法

  1. 使用ThreadLocal的set方法存入value,实际上是获取当前线程的ThreadLocalMap对象,将threadLocal对象作为一个key存入到map中。
//java.lang.ThreadLocal#set
public void set(T value) {  
    //获取当前线程
    Thread t = Thread.currentThread();  
    //获取当前线程的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);  
    if (map != null)  
        //将threadLocal作为key,和value存入到ThreadLocalMap中
        map.set(this, value);  
    else  
        createMap(t, value);  
}  
  1. 现在的思路就是调用map的set方法。

Map底层就是一个数组,HashMap使用数组+链表的结构,其实是使用哈希桶去解决哈希冲突问题。而ThreadLocal自己实现Map结构,采用线性探测法去解决哈希冲突,所以单纯的使用数组便可以实现Map结构。

map中元素是依靠hashCode去计算在数组的位置的。但是总会有一些元素它们并不相等,但是他们的hashCode相同,即在数组中的位置相同。

static class Entry extends WeakReference> {  
    /** The value associated with this ThreadLocal. */  
    Object value;  
  
    Entry(ThreadLocal> k, Object v) {  
        super(k);  
        value = v;  
    }  
}  

数组中的对象为Entry对象,Entry对象有两个属性,一个是弱引用持有的key,一个是强引用持有的value。

当key只被弱引用持有,并且发送了GC,key就会被回收,所以在Entry[]中会存在一些失效节点。ThreadLocalMap考虑到了这种情况,每次set的过程中,都会清除失效节点。这种补救措施便可以使得用户未显式调用reomve方法时,线程中的失效entry在set操作时也会被清理掉。


  • 颜色相同的节点表示HashCode相同;
  • 里面的值若是相同,代表两个节点的key完全相同;
JAVA并发(4)— ThreadLocal源码角度分析是否真正能造成内存溢出!
image.png
private void set(ThreadLocal> key, Object value) {  
 
    Entry[] tab = table;  
    int len = tab.length;  
     //通过key的HashCode计算在map中的下标位置;
    int i = key.threadLocalHashCode & (len-1);  
    //情况1:下标位置存在元素。
    for (Entry e = tab[i];  
         e != null;  
         e = tab[i = nextIndex(i, len)]) {  
        ThreadLocal> k = e.get();  
        //若key相等,则去覆盖
        if (k == key) {  
            e.value = value;  
            return;  
        }  
        //若发现key为null,但是entry存在的节点,即已失效的节点
        if (k == null) {  
            //替换失效的节点。
            replaceStaleEntry(key, value, i);  
            return;  
        }  
    }  
    //若位置不存在元素,则生成entry对象,放入到map中。
    tab[i] = new Entry(key, value);  
    int sz = ++size;  
    if (!cleanSomeSlots(i, sz) && sz >= threshold)  
        rehash();  
}  
JAVA并发(4)— ThreadLocal源码角度分析是否真正能造成内存溢出!
遇到失效节点.png
private void replaceStaleEntry(ThreadLocal> key, Object value,  
                               int staleSlot) {  
    Entry[] tab = table;  
    int len = tab.length;  
    Entry e;  
     //往前执行,寻找失效节点的范围
    int slotToExpunge = staleSlot;  
    for (int i = prevIndex(staleSlot, len);  
         (e = tab[i]) != null;  
         i = prevIndex(i, len))  
        if (e.get() == null)  
            slotToExpunge = i;  
   //若是在往后寻找的过程中,遇到key相等的节点,则与覆盖该节点并与失效节点交换
    for (int i = nextIndex(staleSlot, len);  
         (e = tab[i]) != null;  
         i = nextIndex(i, len)) {  
        ThreadLocal> k = e.get();  
       
        if (k == key) {  
            e.value = value;  
  
            tab[i] = tab[staleSlot];  
            tab[staleSlot] = e;  
  
            // 开始清除失效节点
            if (slotToExpunge == staleSlot)  
                slotToExpunge = i;  
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);  
            return;  
        }  
  
        if (k == null && slotToExpunge == staleSlot)  
            slotToExpunge = i;  
    }  
    //若是找到空节点,这将失效节点置空,并将值覆盖到失效节点上。
    tab[staleSlot].value = null;  
    tab[staleSlot] = new Entry(key, value);  
  
    if (slotToExpunge != staleSlot)  
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);  
}  

1.2 ThreadLocal的get方法

我们在线程中使用ThreadLocal.set()方法,是为了在线程运行到某处时调用ThreadLocal.get()方法获取到存储的值。

public T get() {  
    Thread t = Thread.currentThread();  
    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();  
}  
private Entry getEntry(ThreadLocal> key) {  
    //通过HashCode计算出map中的位置
    int i = key.threadLocalHashCode & (table.length - 1);  
    Entry e = table[i];  
    //该位置的条目不为空,并且key相当则返回
    if (e != null && e.get() == key)  
        return e;  
    else  
        return getEntryAfterMiss(key, i, e);  
}  
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;  
}  

总结

ThreadLocal类提供的API方法实际上就是操纵当前线程对象中的ThreadLocalMap属性。ThreadLocalMap底层数据结构就是一个Entry数组对象。Entry有两个属性,key是弱引用持有的ThreadLocal对象,而value为我们存储的值。

在使用set或get方法时,会进行线性探测寻找对应的Entry对象,若发现失效节点,ThreadLocalMap会清除这些节点。但是也可以这样理解,若今后没有在此调用set或get方法,这些value永远不会被清除的。从而造成了内存泄漏。
当内存泄漏积少成多,最终可能会内存溢出。

2.1 ThreadLocalMap的key为什么设置为弱引用

ThreadLocalMap的key就是ThreadLocal对象,它在创建出来时,会被强引用和弱引用同时持有。当线程执行完任务后(伴随方法出栈),ThreadLoca只会被弱引用持有,一旦发生GC,key就会被置为null。而ThreadLocal的set或get操作,在线性探测定位entry时,遇到key==null的节点,会将其看做为失效节点进行回收。

2.2 ThreadLocal为什么是static修饰

优点:使用了static方法,实际上可以避免ThreadLocal对象重复创建;
缺点:使用了static方法,map的key就会被强引用持有,除非显式调用remove()方法,否则key不会被回收。

但是利大于弊。

在Spring的bean中使用ThreadLocal,因为bean大多数是单例,故ThreadLocal有无static修饰效果相同。

2.3 ThreadLocalMap的value为什么不会被回收

该value对象被强引用所持有。所以不会被回收。

2.4 ThreadLocal为什么会造成内存泄漏

TheadLocal是操作当前线程的ThreadLocalMap属性,该map的底层数据结构是Entry数组,Entry的value会强引用着对象。所以该对象不会被回收,造成内存泄漏。

相关阅读

JAVA并发(1)—java对象布局
JAVA并发(2)—PV机制与monitor(管程)机制
JAVA并发(3)—线程运行时发生GC,会回收ThreadLocal弱引用的key吗?
JAVA并发(4)— ThreadLocal源码角度分析是否真正能造成内存溢出!
JAVA并发(5)— 多线程顺序的打印出A,B,C(线程间的协作)
JAVA并发(6)— AQS源码解析(独占锁-加锁过程)
JAVA并发(7)—AQS源码解析(独占锁-解锁过程)
JAVA并发(8)—AQS公平锁为什么会比非公平锁效率低(源码分析)
JAVA并发(9)— 共享锁的获取与释放
JAVA并发(10)—interrupt唤醒挂起线程
JAVA并发(11)—AQS源码Condition阻塞和唤醒
JAVA并发(12)— Lock实现生产者消费者


推荐阅读
  • 在Spring与Ibatis集成的环境中,通过Spring AOP配置事务管理至服务层。当在一个服务方法中引入自定义多线程时,发现事务管理功能失效。若不使用多线程,事务管理则能正常工作。本文深入分析了这一现象背后的潜在风险,并探讨了可能的解决方案,以确保事务一致性和线程安全。 ... [详细]
  • 开发心得:深入探讨Servlet、Dubbo与MyBatis中的责任链模式应用
    开发心得:深入探讨Servlet、Dubbo与MyBatis中的责任链模式应用 ... [详细]
  • 如何在Java中高效构建WebService
    本文介绍了如何利用XFire框架在Java中高效构建WebService。XFire是一个轻量级、高性能的Java SOAP框架,能够简化WebService的开发流程。通过结合MyEclipse集成开发环境,开发者可以更便捷地进行项目配置和代码编写,从而提高开发效率。此外,文章还详细探讨了XFire的关键特性和最佳实践,为读者提供了实用的参考。 ... [详细]
  • PHP中元素的计量单位是什么? ... [详细]
  • 本文详细探讨了Java集合框架的使用方法及其性能特点。首先,通过关系图展示了集合接口之间的层次结构,如`Collection`接口作为对象集合的基础,其下分为`List`、`Set`和`Queue`等子接口。其中,`List`接口支持按插入顺序保存元素且允许重复,而`Set`接口则确保元素唯一性。此外,文章还深入分析了不同集合类在实际应用中的性能表现,为开发者选择合适的集合类型提供了参考依据。 ... [详细]
  • 深入解析JWT的实现与应用
    本文深入探讨了JSON Web Token (JWT) 的实现机制及其应用场景。JWT 是一种基于 RFC 7519 标准的开放性认证协议,用于在各方之间安全地传输信息。文章详细分析了 JWT 的结构、生成和验证过程,并讨论了其在现代 Web 应用中的实际应用案例,为开发者提供了全面的理解和实践指导。 ... [详细]
  • 本文作为“实现简易版Spring系列”的第五篇,继前文深入探讨了Spring框架的核心技术之一——控制反转(IoC)之后,将重点转向另一个关键技术——面向切面编程(AOP)。对于使用Spring框架进行开发的开发者来说,AOP是一个不可或缺的概念。了解AOP的背景及其基本原理,对于掌握这一技术至关重要。本文将通过具体示例,详细解析AOP的实现机制,帮助读者更好地理解和应用这一技术。 ... [详细]
  • 本文深入探讨了 MXOTDLL.dll 在 C# 环境中的应用与优化策略。针对近期公司从某生物技术供应商采购的指纹识别设备,该设备提供的 DLL 文件是用 C 语言编写的。为了更好地集成到现有的 C# 系统中,我们对原生的 C 语言 DLL 进行了封装,并利用 C# 的互操作性功能实现了高效调用。此外,文章还详细分析了在实际应用中可能遇到的性能瓶颈,并提出了一系列优化措施,以确保系统的稳定性和高效运行。 ... [详细]
  • Ceph API微服务实现RBD块设备的高效创建与安全删除
    本文旨在实现Ceph块存储中RBD块设备的高效创建与安全删除功能。开发环境为CentOS 7,使用 IntelliJ IDEA 进行开发。首先介绍了 librbd 的基本概念及其在 Ceph 中的作用,随后详细描述了项目 Gradle 配置的优化过程,确保了开发环境的稳定性和兼容性。通过这一系列步骤,我们成功实现了 RBD 块设备的快速创建与安全删除,提升了系统的整体性能和可靠性。 ... [详细]
  • Spring框架入门指南:专为新手打造的详细学习笔记
    Spring框架是Java Web开发中广泛应用的轻量级应用框架,以其卓越的功能和出色的性能赢得了广大开发者的青睐。本文为初学者提供了详尽的学习指南,涵盖基础概念、核心组件及实际应用案例,帮助新手快速掌握Spring框架的核心技术与实践技巧。 ... [详细]
  • 在稀疏直接法视觉里程计中,通过优化特征点并采用基于光度误差最小化的灰度图像线性插值技术,提高了定位精度。该方法通过对空间点的非齐次和齐次表示进行处理,利用RGB-D传感器获取的3D坐标信息,在两帧图像之间实现精确匹配,有效减少了光度误差,提升了系统的鲁棒性和稳定性。 ... [详细]
  • 如何在 Java LinkedHashMap 中高效地提取首个或末尾的键值对? ... [详细]
  • C#编程指南:实现列表与WPF数据网格的高效绑定方法 ... [详细]
  • MySQL性能优化与调参指南【数据库管理】
    本文详细探讨了MySQL数据库的性能优化与参数调整技巧,旨在帮助数据库管理员和开发人员提升系统的运行效率。内容涵盖索引优化、查询优化、配置参数调整等方面,结合实际案例进行深入分析,提供实用的操作建议。此外,还介绍了常见的性能监控工具和方法,助力读者全面掌握MySQL性能优化的核心技能。 ... [详细]
  • 深入解析Java中HashCode的功能与应用
    本文深入探讨了Java中HashCode的功能与应用。在Java中,HashCode主要用于提高哈希表(如HashMap、HashSet)的性能,通过快速定位对象存储位置,减少碰撞概率。文章详细解析了HashCode的生成机制及其在集合框架中的作用,帮助开发者更好地理解和优化代码。此外,还介绍了如何自定义HashCode方法以满足特定需求,并讨论了常见的实现误区和最佳实践。 ... [详细]
author-avatar
草莓公主滴窝窝
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有