首页 > 编程语言 >java中ThreadLocal的基本原理

java中ThreadLocal的基本原理

时间:2023-02-06 18:14:23浏览次数:33  
标签:java 对象 基本原理 回收 ThreadLocal 引用 Entry 内存

目录
  • 基本流程
  • ThreadLoalMap数据结构
    • Hash冲突及解决
  • ThreadLocal内存泄露
    • 内存引用链路
    • 引用类型
  • 为什么使用弱引用而不是强引用?
  • 泄露原因分析
  • ThreadLocal应用场景

 

源码实现

一个线程内可以存多个ThreadLocal对象,存储的位置位于Thread的ThreadLocal.ThreadLocalMap变量,在Thread中有如下变量:



ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap是由ThreadLocal维护的静态内部类,正如代码中注解所说这个变量是由ThreadLocal维护的。

 

基本流程

在这里插入图片描述

 

ThreadLoalMap数据结构

ThreadLoalMap是ThreadLocal中的一个静态内部类,类似HashMap的数据结构,但并没有实现Map接口。


static class ThreadLocalMap {
 
    static class Entry extends WeakReference<ThreadLocal<?>> {
        
        Object value;
 
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
 
    private static final int INITIAL_CAPACITY = 16;
 
    // ...
}

ThreadLoalMap中初始化了一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对。通过上面的set方法,我们已经知道其中的key永远都是ThreadLocal对象。

在这里插入图片描述

 

Hash冲突及解决

Entry在table中存储位置是通过hashcode算法获得。 在向ThreadLocalMap中的Entry数值存储Entry对象时,会根据ThreadLocal对象的hash值,定位到table中的位置i。分三种情况:

  • 如果当前位置为空的,直接将Entry存放在对应位置;
  • 如果位置i已经有值且这个Entry对象的key正好是即将设置的key,那么重新设置Entry中的value;
  • 如果位置i的Entry对象和即将设置的key没关系,则寻找一个空位置;

计算hash值便会有hash冲突出现,常见的解决方法有:再哈希法、开放地址法、建立公共溢出区、链式地址法等。

上面的流程可以看出这里采用的是开放地址方法,如果当前位置有值,就继续寻找下一个位置,注意table[len-1]的下一个位置是table[0],就像是一个环形数组,所以也叫闭散列法。 如果一直都找不到空位置就会出现死循环,发生内存溢出。当然有扩容机制,一般不会找不到空位置的。

 

ThreadLocal内存泄露

 

内存引用链路

根据前面对ThreadLocal的分析,得知每个Thread维护一个ThreadLocalMap,它key是ThreadLocal实例本身,value是业务需要存储的Object。也就是说ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。

ThreadLocalMap是使用ThreadLocal的弱引用作为Key的,弱引用的对象在GC时会被回收。因此使用了ThreadLocal后,引用链如图所示:(其中虚线表示弱引用。)

在这里插入图片描述

 

引用类型

强引用:java默认的引用类型,例如 Object a = new Object();其中 a 为强引用,new Object()为一个具体的对象。一个对象从根路径能找到强引用指向它,JVM虚拟机就不会回收。

软引用(SoftReference):进行年轻代的垃圾回收不会触发SoftReference所指向对象的回收;但如果触发Full GC,那SoftReference所指向的对象将被回收。备注:是除了软引用之外没有其他强引用引用的情况下。

弱引用(WeakReference) :如果对象除了有弱引用指向它后没有其他强引用关联它,当进行年轻代垃圾回收时,该引用指向的对象就会被垃圾回收器回收。

虚引用(PhantomeReference) :该引用指向的对象,无法对垃圾收集器收集对象时产生任何影响,但在执行垃圾回收后垃圾收集器会通过注册在PhantomeReference上的队列来通知应用程序对象被回收。

 

为什么使用弱引用而不是强引用?

问题1:从表面上看内存泄漏的根源在于使用了弱引用,但为什么jdk采用了弱引用的实现而不是强引用呢?

答案是:弱引用反而是为了解决内存存储问题而专门使用的。

问题2:如果应用程序觉得ThreadLocal对象的使命完成,将threadLocal ref 设置为null,如果Entry中引用ThreadLocald对象的引用类型设置为强引用的话,会发生什么问题?

答案是:ThreadLocal对象会无法被垃圾回收器回收,因为从thread对象出发,有强引用指向ThreadLocal的object。此时会违背用户的初衷,造成所谓的内存泄露。

我们先来假设一下,如果key使用强引用,那么在其他持有ThreadLocal引用的对象都回收了,但ThreadLocalMap依旧持有ThreadLocal的强引用,这就导致ThreadLocal不会被回收,从而导致Entry内存泄露。

对照一下,弱引用的情况。持有ThreadLocal引用的对象都回收了,ThreadLocalMap持有的是ThreadLocal的弱引用,会被自动回收。只不过对应的value值,需要在下次调用set/get/remove方法时会被清除。

 

泄露原因分析

当Thread执行完会被销毁,Thread.threadLocals指向的ThreadLocalMap实例也随之变为垃圾,它里面存放的Entity也会被回收。这种情况是不会发生内存泄漏的。

发生内存泄露的场景一般存在于线程池的情况下。 此时,Thread生命周期比较长(存在循环使用),threadLocals引用一直存在,当其存放的ThreadLocal被回收(弱引用生命周期比较短)后,对应的Entity就成了key为null的实例,但value值不会被回收。 如果此Entity一直不被get()、set()、remove(),就一直不会被回收,也就发生了内存泄漏。

所以,通常在使用完ThreadLocal后需要调用remove()方法进行内存的清除。

接下来我们再延伸一下,想再来谈谈网络上关于ThreadLocalMap中存储大量Entry对象导致的内存“泄露”问题?

网络观点:在使用ThreadLocal中set方法与remove方法需要成对执行,需要没有执行remove方法会造成内存泄露?甚至造成内存溢出?

我的观点:当然能成对使用当然更好,但在实际情况中,其实不调用remove方法也不太容易造成内存溢出,因为从存储结构来看,除非创建海量线程,并且这些线程都不释放,导致大量线程内部持有的ThreadLocalMap中对象一直不会释放,但一个线程所持有的Entry对象个数不多,取决于关联的ThreadLocal对象个数,故我们需要的关注点而不是remove方法,而是防止线程资源泄露。

 

ThreadLocal应用场景

  • 线程间数据隔离,各线程的ThreadLocal互不影响;
  • 方便同一个线程使用某一对象,避免不必要的参数传递;
  • 全链路追踪中的traceId或者流程引擎中上下文的传递一般采用ThreadLocal;
  • spring事务管理器采用了ThreadLocal;
  • Spring mvc的RequestContextHolder的实现使用了ThreadLocal;

标签:java,对象,基本原理,回收,ThreadLocal,引用,Entry,内存
From: https://www.cnblogs.com/dituirenwu/p/17096310.html

相关文章

  • java 8 stream 使用及其技巧
    java8stream使用及其技巧 什么是流?先来看看Pig upms中的使用@Override@Transactional(rollbackFor=Exception.class)publicBooleansaveUs......
  • Java中Lambda表达式基础及使用
    目录一、举例说明1、无参无返回1.1定义一个接口1.2接口实现类1.3测试类2、有参无返回代码示例3、有参有返回二、简单事项1、省略模式2、注意事项三、L......
  • final关键字--Java基础052
    /*final(最终、修饰符)final关键字的用法:1.final关键字修饰一个基本类型的变量时,该变量不能重新赋值,第一次的值为最终的。2.fianl关键字修饰一个引用类型变量时,该......
  • 对象创建的次数--Java基础050
    /*需求:统计一个类被使用了多少次创建对象,该类对外显示被创建的次数。*/classEmp{//非静态的成员变量。staticintcount=0;//计数器Stringname;//构造......
  • [Ubuntu]11.04 下安装 JAVA 方法
    UbuntuJDK安装配置的详细步骤:UbuntuJDK安装配置1.下载jdk下载连接http://java.sun.com/javase/downloads/index.jsp选择jdk-6u14-linux-i586.bin......
  • instanceof关键字--Java基础051
    /*instanceof关键字instanceof关键字的作用:判断一个对象是否属于指定的类别。instanceof关键字的使用前提:判断的对象与指定的类别必须要存在继承或者实现的关系。instanceo......
  • 重写--Java基础049
    /*目前的问题:父类的功能无法满足子类的需求。方法重写的前提:必须要存在继承的关系。方法的重写:子父类出了同名的函数,这个我们就称作为方法的重写。什么是时候要使用方法......
  • 继承--Java基础047
    /*面向对象的三大特征:1.封装2.继承3.多态.继承:继承是通过关键字extends体现的。继承的格式:class类名1extends类名2{}继承要注意的事项:1.千万不要为了......
  • java补充知识:权限修饰符&代码块&抽象类
    目录final修饰方法修饰类变量常量的使用细则权限修饰符的分类实际使用中修饰符的使用代码块局部代码块构造代码块静态代码块抽象类抽象类与抽象方法的定义格式抽象类和抽象......
  • String类构造方法与普通方法--Java基础058
    packagetest;publicclassDemo1{publicstaticvoidmain(String[]args){//1对象的比较Stringstr1="hello";Stringstr2="hello";......