【Java并发编程】导致JVM内存泄露的ThreadLocal详解

ThreadLocal及内存泄漏

(1)说明
ThreadLocal为每个线程都提供了变量的副本(ThreadLocalMap),使得每个线程在某一时间访问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享;
ThreadLocal和Synchonized区别:
ThreadLocal和Synchonized都用于解决多线程并发访问。可是ThreadLocal与synchronized有本质的差别。synchronized是利用锁的机制,使变量或代码块在某一时该仅仅能被一个线程访问,ThreadLocal则是副本机制,此时不论多少线程并发访问都是线程安全的;
性能比较:ThreadLocal > AtomicInteger > ReentrantLock,因为ThreadLocal没有锁竞争;
(2)数据结构
【Java并发编程】导致JVM内存泄露的ThreadLocal详解_第1张图片
一个当前线程对应一个ThreadLocalMap,ThreadLocalMap是ThreadLocal的静态内部类,用Entry数组来进行存储具体数据,Entry的key为ThreadLocal,value为存的值,ThreadLocalMap实例化是在Thread内部;
Entry的key(ThreadLocal)为WeakReference弱引用,通过Entry数组来存放线程可能需要的多个副本变量;
(3)作用及用处

  • 线程间数据隔离,比如controller层使用ThreadLocal存储请求参数;
  • Spring的事务管理器就是利用ThreadLocal将连接绑定到线程的,从而保证事务生效;
  • 在微服务领域,链路跟踪中的traceId传递也是利用了ThreadLocal;
  • session会话管理;

(4)ThreadLocal使用
ThreadLocal类接口很简单,只有4个方法:

  • void set(Object value)

设置当前线程的线程局部变量的值。

  • public Object get()

该方法返回当前线程所对应的线程局部变量。

  • public void remove()

将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。

  • protected Object initialValue()

返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
(5)ThreadLocal内存泄漏
【Java并发编程】导致JVM内存泄露的ThreadLocal详解_第2张图片

  • 场景

线程池中的线程可以被重复利用,当ThreadLocal为null,要被垃圾收集器回收掉。但此时ThreadLocalMap的生命周期和Thread一样,它不会被回收。此时,ThreadLocalMap的key没了,但是value还在,造成了内存泄漏;

  • 解决

使用线程池时,使用完ThreadLocal后,执行remove操作,避免出现内存泄漏;

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

下面我们分两种情况讨论:
key 使用强引用:对ThreadLocal对象实例的引用被置为null了,但是ThreadLocalMap还持有这个ThreadLocal对象实例的强引用,如果没有手动删除,ThreadLocal的对象实例不会被回收,导致Entry内存泄漏。
key 使用弱引用:对ThreadLocal对象实例的引用被被置为null了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal的对象实例也会在下次gc时被回收。而value在下一次ThreadLocalMap调用set,get,remove都有机会被回收。
比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障。
(6)hash冲突及解决

  • 什么是hash冲突

Hash简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。常用HASH函数:直接取余法、乘法取整法、平方取中法。 Java里的HashMap用的就是直接取余法。

  • 常见处理hash冲突方法

开放定址法
基本思想是,出现冲突后按照一定算法查找一个空位置存放,根据算法的不同又可以分为线性探测再散列、二次探测再散列、伪随机探测再散列。
线性探测再散列即依次向后查找,二次探测再散列,即依次向前后查找,增量为1、2、3的二次方,伪随机,顾名思义就是随机产生一个增量位移。
ThreadLocal里用的则是线性探测再散列;
链地址法
这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。Java里的HashMap用的就是链地址法,为了避免hash 洪水攻击,1.8版本开始还引入了红黑树。
再哈希法
这种方法是同时构造多个不同的哈希函数:Hi=RH1(key) i=1,2,…,k当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。
建立公共溢出区
这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。
(7)总结

  • JVM利用设置ThreadLocalMap的Key为弱引用,来避免ThreadLocal对象内存泄露。
  • JVM利用调用remove、get、set方法的时候,回收弱引用。
  • 当ThreadLocal存储很多Key为null的Entry的时候,而不再去调用remove、get、set方法,那么将导致内存泄漏。
  • 使用线程池+ThreadLocal时要小心,因为这种情况下,线程是一直在不断的重复运行的,从而也就造成了value可能造成累积的情况。

你可能感兴趣的:(#,Java基础,java,开发语言,ThreadLocal,并发编程,内存泄漏)