热门标签 | 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;及时关闭连接


推荐阅读
  • 本文介绍了Python爬虫技术基础篇面向对象高级编程(中)中的多重继承概念。通过继承,子类可以扩展父类的功能。文章以动物类层次的设计为例,讨论了按照不同分类方式设计类层次的复杂性和多重继承的优势。最后给出了哺乳动物和鸟类的设计示例,以及能跑、能飞、宠物类和非宠物类的增加对类数量的影响。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 本文探讨了C语言中指针的应用与价值,指针在C语言中具有灵活性和可变性,通过指针可以操作系统内存和控制外部I/O端口。文章介绍了指针变量和指针的指向变量的含义和用法,以及判断变量数据类型和指向变量或成员变量的类型的方法。还讨论了指针访问数组元素和下标法数组元素的等价关系,以及指针作为函数参数可以改变主调函数变量的值的特点。此外,文章还提到了指针在动态存储分配、链表创建和相关操作中的应用,以及类成员指针与外部变量的区分方法。通过本文的阐述,读者可以更好地理解和应用C语言中的指针。 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • 本文介绍了Java的集合及其实现类,包括数据结构、抽象类和具体实现类的关系,详细介绍了List接口及其实现类ArrayList的基本操作和特点。文章通过提供相关参考文档和链接,帮助读者更好地理解和使用Java的集合类。 ... [详细]
  • 标题: ... [详细]
  • 本文介绍了iOS数据库Sqlite的SQL语句分类和常见约束关键字。SQL语句分为DDL、DML和DQL三种类型,其中DDL语句用于定义、删除和修改数据表,关键字包括create、drop和alter。常见约束关键字包括if not exists、if exists、primary key、autoincrement、not null和default。此外,还介绍了常见的数据库数据类型,包括integer、text和real。 ... [详细]
  • 集合的遍历方式及其局限性
    本文介绍了Java中集合的遍历方式,重点介绍了for-each语句的用法和优势。同时指出了for-each语句无法引用数组或集合的索引的局限性。通过示例代码展示了for-each语句的使用方法,并提供了改写为for语句版本的方法。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
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社区 版权所有