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

Java并发编程实战——并发容器之ThreadLocal及其内存泄漏问题

文章目录ThreadLocal的简介ThreadLocal的实现原理ThreadLocalMap详解ThreadLocal内存泄漏问题ThreadLocal的使用场景ThreadL


文章目录

  • ThreadLocal的简介
  • ThreadLocal的实现原理
  • ThreadLocalMap详解
    • ThreadLocal内存泄漏问题
  • ThreadLocal的使用场景


ThreadLocal的简介

之前写过用ThreadLocal做RabbitMQ的批量发送的文章,这里再深入了解一下。


  • 总的来说,ThreadLocal有什么作用呢?
    主要作用就是以“空间换时间”:通过各个线程自己的ThreadLocalMap来隔离资源,这样就不会出现线程安全问题,从而减少线程阻塞得情况,能使得各自的线程独自高效得处理自己的事情

在多线程编程中通常解决线程安全的问题我们会利用synchronzed或者lock控制线程对临界区资源的同步顺序从而解决线程安全的问题,但是这种加锁的方式会让未获取到锁的线程进行阻塞等待,很显然这种方式的时间效率并不是很好。线程安全问题的核心在于多个线程会对同一个临界区共享资源进行操作,那么,如果每个线程都使用自己的“共享资源”,各自使用各自的,又互相不影响到彼此即让多个线程间达到隔离的状态,这样就不会出现线程安全的问题。

虽然ThreadLocal并不在java.util.concurrent包中而在java.lang包中,但我更倾向于把它当作是一种并发容器(虽然真正存放数据的是ThreadLocalMap)进行归类。从ThreadLocal这个类名可以顾名思义的进行理解,表示线程的“本地变量”,即每个线程都拥有该变量副本,达到人手一份的效果,各用各的这样就可以避免共享资源的竞争。


所以接下来主要需要关注ThreadLocalMap。



ThreadLocal的实现原理

要想学习到ThreadLocal的实现原理,就必须了解它的几个核心方法,包括怎样存怎样取等等,下面我们一个个来看。
set方法设置在当前线程中threadLocal变量的值,该方法的源码为:

public void set(T value) {//1. 获取当前线程实例对象Thread t = Thread.currentThread();//2. 通过当前线程实例获取到ThreadLocalMap对象ThreadLocalMap map = getMap(t);if (map != null)//3. 如果Map不为null,则以当前threadLocl实例为key,值为value进行存入map.set(this, value);else//4.map为null,则新建ThreadLocalMap并存入valuecreateMap(t, value);
}

方法的逻辑很清晰,具体请看上面的注释。通过源码我们知道value是存放在了ThreadLocalMap里了,当前先把它理解为一个普普通通的map即可,也就是说,数据value是真正的存放在了ThreadLocalMap这个容器中了,并且是以当前threadLocal实例为key(并不是以Current Thread为key,这里要好好理解,一个程序可能有多个不同作用的ThreadLocal,每个ThreadLocal以自身为key来存储Object(value),这些Object类型可以是List,也可以是Integer等等)。

先简单的看下ThreadLocalMap是什么,有个简单的认识就好,下面会具体说的。
首先ThreadLocalMap是怎样来的?源码很清楚,是通过getMap(t)进行获取:

ThreadLocalMap getMap(Thread t) {return t.threadLocals;
}

该方法直接返回的就是当前线程对象t的一个成员变量threadLocals:

# Thread.class
/* ThreadLocal values pertaining to this thread. This map is maintained* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

也就是说ThreadLocalMap的引用是作为Thread的一个成员变量,被Thread进行维护的(也就是说,多个Thread拥有多个ThreadLocalMap对象,这是资源隔离的基础)

现在来对set方法进行总结一下:
通过当前线程对象thread获取该thread所维护的threadLocalMap,若threadLocalMap不为null,则以threadLocal实例为key,值为value的键值对存入threadLocalMap,若threadLocalMap为null的话,就新建threadLocalMap然后在以threadLocal为键,值为value的键值对存入即可。

get方法是获取当前线程中threadLocal变量的值,同样的还是来看看源码:

public T get() {//1. 获取当前线程的实例对象Thread t = Thread.currentThread();//2. 获取当前线程的threadLocalMapThreadLocalMap map = getMap(t);if (map != null) {//3. 获取map中当前threadLocal实例为key的值的entryThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")//4. 当前entitiy不为null的话,就返回相应的值valueT result = (T)e.value;return result;}}//5. 若map为null或者entry为null的话通过该方法初始化,并返回该方法返回的valuereturn setInitialValue();
}

弄懂了set方法的逻辑,看get方法只需要带着逆向思维去看就好,如果是那样存的,反过来去拿就好。关于get方法来总结一下:
通过当前线程thread实例获取到它所维护的threadLocalMap,然后以当前threadLocal实例为key获取该map中的键值对(Entry),若Entry不为null则返回Entry的value。如果获取threadLocalMap为null或者Entry为null的话,就以当前threadLocal为Key,value为null存入map后,并返回null。


ThreadLocalMap详解

从上面的分析我们已经知道,数据其实都放在了threadLocalMap中,threadLocal的get,set和remove方法实际上具体是通过threadLocalMap的getEntry,set和remove方法实现的。如果想真正全方位的弄懂threadLocal,势必得在对threadLocalMap做一番理解。

ThreadLocalMap是threadLocal一个静态内部类,和大多数容器一样内部维护了一个数组,同样的threadLocalMap内部维护了一个Entry类型的table数组。

/*** The table, resized as necessary.* table.length MUST always be a power of two.*/
private Entry[] table;

通过注释可以看出,table数组的长度为2的幂次方。接下来看下Entry是什么:

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

Entry是一个以ThreadLocal为key,Object为value的键值对&#xff0c;另外需要注意的是这里的threadLocal是弱引用&#xff0c;因为Entry继承了WeakReference&#xff0c;在Entry的构造方法中&#xff0c;调用了super(k)方法就会将threadLocal实例包装成一个WeakReferenece。


ThreadLocal内存泄漏问题

到这里我们可以用一个图来理解下thread,threadLocal,threadLocalMap&#xff0c;Entry之间的关系&#xff1a;
在这里插入图片描述
注意上图中的实线表示强引用&#xff0c;虚线表示弱引用。

我们可以从两个关注点来理解这张图&#xff1a;


  1. ThreadLocal Ref->ThreadLocal这是栈指向堆的一个强引用&#xff0c;而threadLocal到threadLocalMap是弱引用关系。
  2. 以及强引用链&#xff1a;CurrentThread Ref -> CurrentThread -> ThreaLocalMap -> Entry -> value。

如上图所示&#xff0c;每个线程实例中可以通过threadLocals获取到threadLocalMap&#xff0c;而threadLocalMap实际上就是一个以threadLocal实例为key&#xff0c;任意对象为value的Entry数组。当我们为threadLocal变量赋值&#xff0c;实际上就是以当前threadLocal实例为key&#xff0c;值为value的Entry往这个threadLocalMap中存放。

需要注意的是Entry中的key是弱引用&#xff0c;当threadLocal外部强引用被置为null(threadLocalInstance&#61;null),那么系统 GC 的时候&#xff0c;根据可达性分析&#xff0c;这个threadLocal实例就没有任何一条链路能够引用到它&#xff0c;这个ThreadLocal势必会被回收&#xff0c;这样一来&#xff0c;ThreadLocalMap中就会出现key为null的Entry&#xff0c;就没有办法访问这些key为null的Entry的value&#xff0c;如果当前线程再迟迟不结束的话&#xff0c;这些key为null的Entry的value就会一直存在一条强引用链&#xff1a;CurrentThread Ref -> CurrentThread -> ThreaLocalMap -> Entry -> value永远无法回收&#xff0c;造成内存泄漏

当然&#xff0c;如果当前thread运行结束&#xff0c;threadLocal&#xff0c;threadLocalMap,Entry没有引用链可达&#xff0c;在垃圾回收的时候都会被系统进行回收。在实际开发中&#xff0c;会使用线程池去维护线程的创建和复用&#xff0c;比如固定大小的线程池&#xff0c;线程为了复用是不会主动结束的&#xff0c;所以&#xff0c;threadLocal的内存泄漏问题&#xff0c;是应该值得我们思考和注意的问题

为了优化内存泄漏问题&#xff0c;ThreadLocal自身做了努力&#xff0c;这里我们来看看set方法&#xff1a;

private void set(ThreadLocal<?> key, Object value) {// We don&#39;t use a fast path as with get() because it is at// least as common to use set() to create new entries as// it is to replace existing ones, in which case, a fast// path would fail more often than not.Entry[] tab &#61; table;int len &#61; tab.length;//根据threadLocal的hashCode确定Entry应该存放的位置int i &#61; key.threadLocalHashCode & (len-1);//采用开放地址法&#xff0c;hash冲突的时候使用线性探测for (Entry e &#61; tab[i];e !&#61; null;e &#61; tab[i &#61; nextIndex(i, len)]) {ThreadLocal<?> k &#61; e.get();//覆盖旧Entryif (k &#61;&#61; key) {e.value &#61; value;return;}//当key为null时&#xff0c;说明threadLocal强引用已经被释放掉&#xff0c;那么就无法//再通过这个key获取threadLocalMap中对应的entry&#xff0c;这里就存在内存泄漏的可能性if (k &#61;&#61; null) {//用当前插入的值替换掉这个key为null的“脏”entryreplaceStaleEntry(key, value, i);return;}}//新建entry并插入table中i处tab[i] &#61; new Entry(key, value);int sz &#61; &#43;&#43;size;//插入后再次清除一些key为null的“脏”entry,如果大于阈值就需要扩容if (!cleanSomeSlots(i, sz) && sz >&#61; threshold)rehash();
}

怎样解决“脏”Entry&#xff08;也就是解决内存泄漏&#xff09;&#xff1f;

在分析threadLocal,threadLocalMap以及Entry的关系的时候&#xff0c;我们已经知道使用threadLocal有可能存在内存泄漏&#xff08;对象创建出来后&#xff0c;在之后的逻辑一直没有使用该对象&#xff0c;但是垃圾回收器无法回收这个部分的内存&#xff09;&#xff0c;在源码中针对这种key为null的Entry称之为“stale entry”&#xff0c;直译为不新鲜的entry&#xff0c;我把它理解为“脏entry”&#xff0c;在set方法的for循环中寻找和当前Key相同的可覆盖entry的过程中通过replaceStaleEntry方法解决脏entry的问题。如果当前table[i]为null的话&#xff0c;直接插入新entry后也会通过cleanSomeSlots来解决脏entry的问题&#xff0c;关于cleanSomeSlots和replaceStaleEntry方法&#xff0c;会在详解threadLocal内存泄漏中讲到&#xff0c;具体可看那篇文章


ThreadLocal的使用场景

ThreadLocal 不是用来解决共享对象的多线程访问问题的&#xff0c;数据实质上是放在每个thread实例引用的threadLocalMap,也就是说每个不同的线程都拥有专属于自己的数据容器&#xff08;threadLocalMap&#xff09;&#xff0c;彼此不影响。因此threadLocal只适用于 共享对象会造成线程安全 的业务场景。比如hibernate中通过threadLocal管理Session就是一个典型的案例&#xff0c;不同的请求线程&#xff08;用户&#xff09;拥有自己的session,若将session共享出去被多线程访问&#xff0c;必然会带来线程安全问题。下面&#xff0c;我们自己来写一个例子&#xff0c;SimpleDateFormat.parse方法会有线程安全的问题&#xff0c;我们可以尝试使用threadLocal包装SimpleDateFormat&#xff0c;将该实例不被多线程共享即可。

public class ThreadLocalDemo {private static ThreadLocal<SimpleDateFormat> sdf &#61; new ThreadLocal<>();public static void main(String[] args) {ExecutorService executorService &#61; Executors.newFixedThreadPool(10);for (int i &#61; 0; i < 100; i&#43;&#43;) {executorService.submit(new DateUtil("2019-11-25 09:00:" &#43; i % 60));}}static class DateUtil implements Runnable {private String date;public DateUtil(String date) {this.date &#61; date;}&#64;Overridepublic void run() {if (sdf.get() &#61;&#61; null) {sdf.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));} else {try {Date date &#61; sdf.get().parse(this.date);System.out.println(date);} catch (ParseException e) {e.printStackTrace();}}}}
}

如果当前线程不持有SimpleDateformat对象实例&#xff0c;那么就新建一个并把它设置到当前线程中&#xff0c;如果已经持有&#xff0c;就直接使用。另外&#xff0c;从if (sdf.get() &#61;&#61; null){…}else{…}可以看出为每一个线程分配一个SimpleDateformat对象实例是从应用层面&#xff08;业务代码逻辑&#xff09;去保证的。
在上面我们说过threadLocal有可能存在内存泄漏&#xff0c;在使用完之后&#xff0c;最好使用remove方法将这个变量移除&#xff0c;就像在使用数据库连接一样&#xff0c;及时关闭连接


推荐阅读
  • 本文详细探讨了在微服务架构中,使用Feign进行远程调用时出现的请求头丢失问题,并提供了具体的解决方案。重点讨论了单线程和异步调用两种场景下的处理方法。 ... [详细]
  • 深入解析SpringMVC核心组件:DispatcherServlet的工作原理
    本文详细探讨了SpringMVC的核心组件——DispatcherServlet的运作机制,旨在帮助有一定Java和Spring基础的开发人员理解HTTP请求是如何被映射到Controller并执行的。文章将解答以下问题:1. HTTP请求如何映射到Controller;2. Controller是如何被执行的。 ... [详细]
  • 在高并发需求的C++项目中,我们最初选择了JsonCpp进行JSON解析和序列化。然而,在处理大数据量时,JsonCpp频繁抛出异常,尤其是在多线程环境下问题更为突出。通过分析发现,旧版本的JsonCpp存在多线程安全性和性能瓶颈。经过评估,我们最终选择了RapidJSON作为替代方案,并实现了显著的性能提升。 ... [详细]
  • 深入解析Spring启动过程
    本文详细介绍了Spring框架的启动流程,帮助开发者理解其内部机制。通过具体示例和代码片段,解释了Bean定义、工厂类、读取器以及条件评估等关键概念,使读者能够更全面地掌握Spring的初始化过程。 ... [详细]
  • 深入解析 Android IPC 中的 Messenger 机制
    本文详细介绍了 Android 中基于消息传递的进程间通信(IPC)机制——Messenger。通过实例和源码分析,帮助开发者更好地理解和使用这一高效的通信工具。 ... [详细]
  • Java多线程实现:从1到100分段求和并汇总结果
    本文介绍如何使用Java编写一个程序,通过10个线程分别计算不同区间的和,并最终汇总所有线程的结果。每个线程负责计算一段连续的整数之和,最后将所有线程的结果相加。 ... [详细]
  • 本文将详细探讨 Java 中提供的不可变集合(如 `Collections.unmodifiableXXX`)和同步集合(如 `Collections.synchronizedXXX`)的实现原理及使用方法,帮助开发者更好地理解和应用这些工具。 ... [详细]
  • 深入解析Java虚拟机(JVM)架构与原理
    本文旨在为读者提供对Java虚拟机(JVM)的全面理解,涵盖其主要组成部分、工作原理及其在不同平台上的实现。通过详细探讨JVM的结构和内部机制,帮助开发者更好地掌握Java编程的核心技术。 ... [详细]
  • 深入解析Java多线程与并发库的应用:空中网实习生面试题详解
    本文详细探讨了Java多线程与并发库的高级应用,结合空中网在挑选实习生时的面试题目,深入分析了相关技术要点和实现细节。文章通过具体的代码示例展示了如何使用Semaphore和SynchronousQueue来管理线程同步和任务调度。 ... [详细]
  • 深入理解Java多线程并发处理:基础与实践
    本文探讨了Java中的多线程并发处理机制,从基本概念到实际应用,帮助读者全面理解并掌握多线程编程技巧。通过实例解析和理论阐述,确保初学者也能轻松入门。 ... [详细]
  • ListView简单使用
    先上效果:主要实现了Listview的绑定和点击事件。项目资源结构如下:先创建一个动物类,用来装载数据:Animal类如下:packagecom.example.simplelis ... [详细]
  • 本文探讨了如何通过一系列技术手段提升Spring Boot项目的并发处理能力,解决生产环境中因慢请求导致的系统性能下降问题。 ... [详细]
  • 烤鸭|本文_Spring之Bean的生命周期详解
    烤鸭|本文_Spring之Bean的生命周期详解 ... [详细]
  • 探讨了一个关于使用多线程实现从0累加至1000的面试题,分析了在不同线程数量下结果出现偏差的原因,并提供了修正方案。 ... [详细]
  • 本文档汇总了Python编程的基础与高级面试题目,涵盖语言特性、数据结构、算法以及Web开发等多个方面,旨在帮助开发者全面掌握Python核心知识。 ... [详细]
author-avatar
黎芝君1_530
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有