专栏首页Java后端技术栈Java多线程编程-(10)-看了这篇关于ThreadLocal的原理应该透彻了

Java多线程编程-(10)-看了这篇关于ThreadLocal的原理应该透彻了

ThreadLocal可以说是笔试面试的常客,每逢面试基本都会问到,关于ThreadLocal的原理以及不正当的使用造成的OOM内存溢出的问题,值得花时间仔细研究一下其原理。这一篇主要学习一下ThreadLocal的原理,在下一篇会深入理解一下OOM内存溢出的原理和最佳实践。

ThreadLocal很容易让人望文生义,想当然地认为是一个“本地线程”。其实,ThreadLocal并不是一个Thread,而是Thread的一个局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

从线程的角度看,目标变量就像是线程的本地变量,这也是类名中“Local”所要表达的意思。

一、ThreadLocal全部方法和内部类

ThreadLocal全部方法和内部类结构如下:

ThreadLocal公有的方法就四个,分别为:get、set、remove、intiValue:

也就是说我们平时使用的时候关心的是这四个方法。

ThreadLocal是如何做到为每一个线程维护变量的副本的呢?

其实实现的思路很简单:在ThreadLocal类中有一个static声明的Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。我们自己就可以提供一个简单的实现版本:

运行结果:

虽然上面的代码清单中的这个ThreadLocal实现版本显得比较简单粗,但其目的主要在与呈现JDK中所提供的ThreadLocal类在实现上的思路。

二、ThreadLocal源码分析

1、线程局部变量在Thread中的位置

既然是线程局部变量,那么理所当然就应该存储在自己的线程对象中,我们可以从 Thread 的源码中找到线程局部变量存储的地方:

我们可以看到线程局部变量是存储在Thread对象的 threadLocals 属性中,而 threadLocals 属性是一个 ThreadLocal.ThreadLocalMap 对象。

ThreadLocalMap为ThreadLocal的静态内部类,如下图所示:

2、Thread和ThreadLocalMap的关系

Thread和ThreadLocalMap的关系,先看下边这个简单的图,可以看出Thread中的threadLocals就是ThreadLocal中的ThreadLocalMap:

到这里应该大致能够感受到上述三者之间微妙的关系,再看一个复杂点的图:

可以看出每个thread实例都有一个ThreadLocalMap。在上图中的一个Thread的这个ThreadLocalMap中分别存放了3个Entry,默认一个ThreadLocalMap初始化了16个Entry,每一个Entry对象存放的是一个ThreadLocal变量对象。

再看一张网络上的图片,应该可以更好的理解,如下图:

这里的Map其实是ThreadLocalMap。

3、ThreadLocalMap与WeakReference

ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以它为Key),不过是经过了两层包装的ThreadLocal对象:

(1)第一层包装是使用 WeakReference<ThreadLocal<?>>ThreadLocal对象变成一个弱引用的对象;

(2)第二层包装是定义了一个专门的类 Entry 来扩展 WeakReference<ThreadLocal<?>>

类 Entry 很显然是一个保存map键值对的实体,ThreadLocal<?>为key, 要保存的线程局部变量的值为valuesuper(k)调用的WeakReference的构造函数,表示将ThreadLocal<?>对象转换成弱引用对象,用做key。

4、ThreadLocalMap 的构造函数

可以看出,ThreadLocalMap这个map的实现是使用一个数组 private Entry[] table 来保存键值对的实体,初始大小为16,ThreadLocalMap自己实现了如何从 key 到 value 的映射:

使用一个 static 的原子属性 AtomicInteger nextHashCode,通过每次增加 HASH_INCREMENT = 0x61c88647 ,然后 & (INITIAL_CAPACITY - 1) 取得在数组 private Entry[] table 中的索引。

总的来说,ThreadLocalMap是一个类似HashMap的集合,只不过自己实现了寻址,也没有HashMap中的put方法,而是set方法等区别。

三、ThreadLocal的set方法

由于每个thread实例都有一个ThreadLocalMap,所以在进行set的时候,首先根据Thread.currentThread()获取当前线程,然后根据当前线程t,调用getMap(t)获取ThreadLocalMap对象, 如果是第一次设置值,ThreadLocalMap对象是空值,所以会进行初始化操作,即调用createMap(t,value)方法:

即是调用上述的构造方法进行构造,这里仅仅是初始化了16个元素的引用数组,并没有初始化16个 Entry 对象。而是一个线程中有多少个线程局部对象要保存,那么就初始化多少个 Entry 对象来保存它们。

到了这里,我们可以思考一下,为什么要这样实现了。

1、为什么要用 ThreadLocalMap 来保存线程局部对象呢?

原因是一个线程拥有的的局部对象可能有很多,这样实现的话,那么不管你一个线程拥有多少个局部变量,都是使用同一个 ThreadLocalMap 来保存的,ThreadLocalMap 中 private Entry[] table 的初始大小是16。超过容量的2/3时,会扩容。

然后在回到如果map不为空的情况,会调用map.set(this, value);方法,我们看到是以当前 thread 的引用为 key, 获得 ThreadLocalMap ,然后调用 map.set(this, value); 保存进 private Entry[] table :

可以看到,set(T value)方法为每个Thread对象都创建了一个ThreadLocalMap,并且将value放入ThreadLocalMap中,ThreadLocalMap作为Thread对象的成员变量保存。那么可以用下图来表示ThreadLocal在存储value时的关系。

2、了解了set方法的大致原理之后,我们在研究一段程序如下:

这样的话就相当于一个线程依附了三个ThreadLocal对象,执行完最后一个set方法之后,调试过程如下:

可以看到table(Entry集合)中有三个对象,对象的值就是我们设置的三个threadLocal的对象值;

3、如果在修改一下代码,修改为两个线程:

这样的话,可以看到运行调试图如下:

然后更改到Thread2,查看,由于多线程,线程1运行到上图情况,线程2运行到下图情况,也可以看出他们是不同的ThreadLocalMap:

那如果多个线程,只设置一个ThreadLocal变量那,结果可想而知,这里不再赘述!

另外,有一点需要提示一下,代码如下:

运行结果:

可以看到,在这个线程中的ThreadLocal变量的值始终是只有一个的,即以前的值被覆盖了的!这里是因为Entry对象是以该ThreadLocal变量的引用为key的,所以多次赋值以前的值会被覆盖,特此注意!

到这里应该可以清楚了的了解Thread、ThreadLocal和ThreadLocalMap之间的关系了!

四、ThreadLocal的get方法

经过上述set方法的分析,对于get方法应该理解起来轻松了许多,首先获取ThreadLocalMap对象,由于ThreadLocalMap使用的当前的ThreadLocal作为key,所以传入的参数为this,然后调用getEntry()方法,通过这个key构造索引,根据索引去table(Entry数组)中去查找线程本地变量,根据下边找到Entry对象,然后判断Entry对象e不为空并且e的引用与传入的key一样则直接返回,如果找不到则调用getEntryAfterMiss()方法。调用getEntryAfterMiss表示直接散列到的位置没找到,那么顺着hash表递增(循环)地往下找,从i开始,一直往下找,直到出现空的槽为止。

五、ThreadLocal的内存回收

ThreadLocal 涉及到的两个层面的内存自动回收:

(1)在 ThreadLocal 层面的内存回收:

当线程死亡时,那么所有的保存在的线程局部变量就会被回收,其实这里是指线程Thread对象中的 ThreadLocal.ThreadLocalMap threadLocals会被回收,这是显然的。

(2)ThreadLocalMap 层面的内存回收:

如果线程可以活很长的时间,并且该线程保存的线程局部变量有很多(也就是 Entry 对象很多),那么就涉及到在线程的生命期内如何回收 ThreadLocalMap 的内存了,不然的话,Entry对象越多,那么ThreadLocalMap 就会越来越大,占用的内存就会越来越多,所以对于已经不需要了的线程局部变量,就应该清理掉其对应的Entry对象。

使用的方式是,Entry对象的key是WeakReference 的包装,当ThreadLocalMap 的 private Entry[] table,已经被占用达到了三分之二时 threshold = 2/3(也就是线程拥有的局部变量超过了10个) ,就会尝试回收 Entry 对象,我们可以看到 ThreadLocalMap.set()方法中有下面的代码:

cleanSomeSlots 就是进行回收内存:

六、ThreadLocal可能引起的OOM内存溢出问题简要分析

我们知道ThreadLocal变量是维护在Thread内部的,这样的话只要我们的线程不退出,对象的引用就会一直存在。当线程退出时,Thread类会进行一些清理工作,其中就包含ThreadLocalMap,Thread调用exit方法如下:

但是,当我们使用线程池的时候,就意味着当前线程未必会退出(比如固定大小的线程池,线程总是存在的)。如果这样的话,将一些很大的对象设置到ThreadLocal中(这个很大的对象实际保存在Thread的threadLocals属性中),这样的话就可能会出现内存溢出的情况。

一种场景就是说如果使用了线程池并且设置了固定的线程,处理一次业务的时候存放到ThreadLocalMap中一个大对象,处理另一个业务的时候,又一个线程存放到ThreadLocalMap中一个大对象,但是这个线程由于是线程池创建的他会一直存在,不会被销毁,这样的话,以前执行业务的时候存放到ThreadLocalMap中的对象可能不会被再次使用,但是由于线程不会被关闭,因此无法释放Thread 中的ThreadLocalMap对象,造成内存溢出。

也就是说,ThreadLocal在没有线程池使用的情况下,正常情况下不会存在内存泄露,但是如果使用了线程池的话,就依赖于线程池的实现,如果线程池不销毁线程的话,那么就会存在内存泄露。所以我们在使用线程池的时候,使用ThreadLocal要格外小心!

七、总结

通过源代码可以看到每个线程都可以独立修改属于自己的副本而不会互相影响,从而隔离了线程和线程.避免了线程访问实例变量发生安全问题. 同时我们也能得出下面的结论:

(1)ThreadLocal只是操作Thread中的ThreadLocalMap对象的集合;

(2)ThreadLocalMap变量属于线程的内部属性,不同的线程拥有完全不同的ThreadLocalMap变量;

(3)线程中的ThreadLocalMap变量的值是在ThreadLocal对象进行set或者get操作时创建的;

(4)使用当前线程的ThreadLocalMap的关键在于使用当前的ThreadLocal的实例作为key来存储value值;

(5) ThreadLocal模式至少从两个方面完成了数据访问隔离,即纵向隔离(线程与线程之间的ThreadLocalMap不同)和横向隔离(不同的ThreadLocal实例之间的互相隔离);

(6)一个线程中的所有的局部变量其实存储在该线程自己的同一个map属性中;

(7)线程死亡时,线程局部变量会自动回收内存;

(8)线程局部变量时通过一个 Entry 保存在map中,该Entry 的key是一个 WeakReference包装的ThreadLocal, value为线程局部变量,key 到 value 的映射是通过:ThreadLocal.threadLocalHashCode & (INITIAL_CAPACITY - 1) 来完成的;

(9)当线程拥有的局部变量超过了容量的2/3(没有扩大容量时是10个),会涉及到ThreadLocalMap中Entry的回收;

对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

参考文章:

1、http://blog.csdn.net/shenlei19911210/article/details/50060223

2、http://www.cnblogs.com/digdeep/p/4510875.html

本文分享自微信公众号 - Java后端技术(JavaITWork),作者:徐刘根

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2017-10-21

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java多线程编程-(11)-面试常客ThreadLocal出现OOM内存溢出的场景和原理分析

    1、首先看一下代码,模拟了一个线程数为500的线程池,所有线程共享一个ThreadLocal变量,每一个线程执行的时候插入一个大的List集合:

    Java后端技术
  • Java多线程编程-(4)-线程本地ThreadLocal的介绍与使用

    我们通过上两篇的学习,我们已经知道了变量值的共享可以使用public static变量的形式,所有的线程都使用同一个被public static修饰的变量。

    Java后端技术
  • Java多线程编程-(9)-使用线程池实现线程的复用和一些坑的避免

    假设这里有一个系统,大概每秒需要处理5万条数据,这5万条数据为一个批次,而这没秒发送的5万条数据数据需要经过两个处理过程,第一步是数据存入数据库,第二步是对数据...

    Java后端技术
  • 抛出这8个问题,检验一下你到底会不会ThreadLocal,来摸个底~

    ThreadLocal类是用来提供线程内部的局部变量。让这些变量在多线程环境下访问(get/set)时能保证各个线程里的变量相对独立于其他线程内的变量。

    Java编程指南
  • ThreadLocal 如此简单

    ThreadLocal提供了线程的局部变量,每个线程都可以通过set()和get()来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程的数...

    大师兄Eric
  • Java源码解析系列-ThreadLocal

    此类提供线程本地变量。这些变量与普通变量不同,因为每个访问一个变量(通过其get或set方法)的线程都有其自己的,独立初始化的变量副本。 ThreadLocal...

    JavaEdge
  • ThreadLocal 核心源码解析

    此类提供线程本地变量。这些变量与普通变量不同,因为每个访问一个变量(通过其get或set方法)的线程都有其自己的,独立初始化的变量副本。 ThreadLocal...

    JavaEdge
  • ThreadLocal到底是什么?它解决了什么问题?

    由于 ThreadLocal 支持范型,如 ThreadLocal< StringBuilder >,为表述方便,后文用 变量代表 ThreadLocal 本身...

    IT大咖说
  • InheritableThreadLocal源码阅读

    在进行多线程编程时,我们经常需要线程池子线程和父线程进行ThreadLocal信息传递,实现一些业务处理。 先看一个例子 public class App { ...

    春哥大魔王
  • 4-07 初次接触ThreadLocal必看

    SuppliedThreadLocal是ThreadLocal的子类,增强了初始化的功能,带有一个初始化的值 ThreadLocalMap是一个自定义的哈希映...

    suveng

扫码关注云+社区

领取腾讯云代金券