ThreadLocal-hash冲突与内存泄漏

el/2024/2/25 23:05:28

ThreadLocal是什么

ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不同的变量值完成操作的场景。

从数据结构入手

下图为ThreadLocal的内部结构图

ThreadLocal结构内部

从上面的结构图,我们已经窥见ThreadLocal的核心机制:

  • 每个Thread线程内部都有一个Map。
  • Map里面存储线程本地对象(key)和线程的变量副本(value)
  • 但是,Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。

所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也独立实现。

在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。但是Entry中key只能是ThreadLocal对象,这点被Entry的构造方法已经限定死了。

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

Entry继承自WeakReference(弱引用,生命周期只能存活到下次GC前),但只有Key是弱引用类型的,Value并非弱引用。

ThreadLocalMap的成员变量:

static class ThreadLocalMap {/*** The initial capacity -- MUST be a power of two.*/private static final int INITIAL_CAPACITY = 16;/*** The table, resized as necessary.* table.length MUST always be a power of two.*/private Entry[] table;/*** The number of entries in the table.*/private int size = 0;/*** The next size value at which to resize.*/private int threshold; // Default to 0
}

Hash冲突怎么解决

和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。

/*** Increment i modulo len.*/
private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);
}/*** Decrement i modulo len.*/
private static int prevIndex(int i, int len) {return ((i - 1 >= 0) ? i - 1 : len - 1);
}

显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。

所以这里引出的良好建议是:每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。

 

ThreadLocalMap的问题

ThreadLocal在ThreadLocalMap中是以一个弱引用身份被Entry中的Key引用的

由于Entry的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露

为什么使用弱引用?

  • key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
  • key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

  • 比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

 

如何避免泄漏
既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。

如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。

ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();
try {threadLocal.set(new Session(1, "Misout的博客"));// 其它业务逻辑
} finally {threadLocal.remove();
}
  • 每个ThreadLocal只能保存一个变量副本,如果想要上线一个线程能够保存多个副本以上,就需要创建多个ThreadLocal。
  • ThreadLocal内部的ThreadLocalMap键为弱引用,会有内存泄漏的风险。


链接:https://www.jianshu.com/p/98b68c97df9b


http://www.ngui.cc/el/4893741.html

相关文章

lambda表达式创建线程、获取cpu的核数、线程睡眠

import java.util.concurrent.TimeUnit; class test1{public static void main(String[] args) {// lambda 表达式创建线程 jdk1.8// lambda表达式: (参数)->{ 代码 }new Thread(()->{try {// 线程睡眠TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.pr…

synchronized 与 Lock

Synchronized 和 Lock 区别 1、Synchronized 内置的Java关键字, Lock 是一个Java类 2、Synchronized 无法判断获取锁的状态,Lock 可以判断是否获取到了锁 3、Synchronized 会自动释放锁,lock 必须要手动释放锁!如果不释放锁,死锁 4、Synchronized 线程 1(获得锁,阻塞)、线程2(等…

集合类-多线程下不安全及其解决办法

1 List 不安全 import java.util.*; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock;…

四大函数式接口(必需掌握)

新时代的程序员: lambda表达式、链式编程、函数式接口、Stream流式计算 1. 函数式接口: 只有一个方法的接口 @FunctionalInterface public interface Runnable {void run(); }// 简化编程模型,在新版本的框架底层大量应用! 只要是 函数型接口 就可以 用 lambda表达式简化: …

ForkJoin与Stream并行流

ForkJoin 在 JDK 1.7 , 并行执行任务!提高效率。大数据量! ForkJoin 特点:工作窃取 这个里面维护的都是双端队列 以下计算1-10_0000_0000的和,for循环与ForkJoin效率差别不大,但是使用Stream并行流效果显著!!! import java.util.*; import java.util.concurrent.Executi…

Volatile-内存屏障

Java 中如何保证底层操作的有序性和可见性&#xff1f;可以通过内存屏障。 内存屏障是被插入两个 CPU 指令之间的一种指令&#xff0c;用来禁止处理器指令发生重排序&#xff08;像屏障一样&#xff09;&#xff0c;从而保障有序性的。 另外&#xff0c;为了达到屏障的效果&a…

单例模式安全之反射攻击

单例模式这里就不谈了&#xff0c;什么是单例模式可参考七种Java单例模式详解,这里是关于单例模式安全方面的&#xff0c;当然了这里说的安全不是线程安全。 什么是反射攻击呢 在Java中&#xff0c;由于反射的功能实在是太强了&#xff0c;通过动态访问类并设置Access(如setA…

ubuntu 解决优盘文件 read-only file system 问题

1.进入root: sudo su2.显示分区&#xff1a; df -Th 结果如下&#xff1a; Filesystem Type Size Used Avail Use% Mounted on udev devtmpfs 3.8G 0 3.8G 0% /dev tmpfs tmpfs 784M 9.5M 775M 2% /run /dev/sdb2 ext4 …

Mybatis Mapper接口是如何找到实现类的-源码分析

转载自:https://www.cnblogs.com/demingblog/p/9544774.html KeyWords: Mybatis 原理,源码,Mybatis Mapper 接口实现类,代理模式,动态代理,Java动态理,Proxy.newProxyInstance,Mapper 映射,Mapper 实现 MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程…

HikariCP连接池监控

Spring-Boot-2.0.0-M1 版本将默认的数据库连接池从tomcat jdbc pool改为了hikari&#xff0c;通过HikariCP可以分析慢查询是什么导致的&#xff01;