阿里架构师教你如何使用ThreadLocal及原理分析

阿里架构师教你如何使用ThreadLocal及原理分析

 

内容导航算法

  • 什么是ThreadLocal
  • ThreadLocal的使用
  • 分析ThreadLocal的实现原理
  • ThreadLocal的应用场景及问题

1、什么是ThreadLocal

ThreadLocal,简单翻译过来就是本地线程,可是直接这么翻译很难理解ThreadLocal的做用,若是换一种说法,能够称为线程本地存储。简单来讲,就是ThreadLocal为共享变量在每一个线程中都建立一个副本,每一个线程能够访问本身内部的副本变量。这样作的好处是能够保证共享变量在多线程环境下访问的线程安全性数据库

2、ThreadLocal的使用演示

ThreadLocal的使用数组

没有使用ThreadLocal时安全

经过一个简单的例子来演示一下ThreadLocal的做用,这段代码是定义了一个静态的成员变量 num,而后经过构造5个线程对这个 num作递增性能优化

阿里架构师教你如何使用ThreadLocal及原理分析

 

运行结果session

阿里架构师教你如何使用ThreadLocal及原理分析

 

每一个线程都会对这个成员变量作递增,若是线程的执行顺序不肯定,那么意味着每一个线程得到的结果也是不同的。多线程

使用了ThreadLocal之后架构

经过ThreadLocal对上面的代码作一个改动并发

阿里架构师教你如何使用ThreadLocal及原理分析

 

运行结果分布式

阿里架构师教你如何使用ThreadLocal及原理分析

 

从结果能够看到,每一个线程的值都是5,意味着各个线程都是从ThreadLocal的 initialValue方法中拿到默认值0而且作了 num+=5的操做,同时也意味着每一个线程从ThreadLocal中拿到的值都是0,这样使得各个线程对于共享变量num来讲,是彻底隔离彼此不相互影响.

ThreadLocal会给定一个初始值,也就是 initialValue()方法,而每一个线程都会从ThreadLocal中得到这个初始化的值的副本,这样可使得每一个线程都拥有一个副本拷贝

3、从源码分析ThreadLocal的实现

看到这里,估计有不少人都会和我同样有一些疑问

  1. 每一个线程的变量副本是怎么存储的?
  2. ThreadLocal是如何实现多线程场景下的共享变量副本隔离?

带着疑问,来看一下ThreadLocal这个类的定义(默认状况下,JDK的源码都是基于1.8版本)

阿里架构师教你如何使用ThreadLocal及原理分析

 

从ThreadLocal的方法定义来看,仍是挺简单的。就几个方法

  • get: 获取ThreadLocal中当前线程对应的线程局部变量
  • set:设置当前线程的线程局部变量的值
  • remove:将当前线程局部变量的值删除

另外,还有一个initialValue()方法,在前面的代码中有演示,做用是返回当前线程局部变量的初始值,这个方法是一个 protected方法,主要是在构造ThreadLocal时用于设置默认的初始值

set方法的实现

set方法是设置一个线程的局部变量的值,至关于当前线程经过set设置的局部变量的值,只对当前线程可见。

阿里架构师教你如何使用ThreadLocal及原理分析

 

  • Thread.currentThread 获取当前执行的线程
  • getMap(t) ,根据当前线程获得当前线程的ThreadLocalMap对象,这个对象具体是作什么的?稍后分析
  • 若是map不为空,说明当前线程已经构造过ThreadLocalMap,直接将值存储到map中
  • 若是map为空,说明是第一次使用,调用 createMap构造

ThreadLocalMap是什么?

咱们来分析一下这句话, ThreadLocalMapmap=getMap(t)得到一个ThreadLocalMap对象,那这个对象是干吗的呢?

其实不用分析,基本上也能猜想出来,Map是一个集合,集合用来存储数据,那么在ThreadLocal中,应该就是用来存储线程的局部变量的。 ThreadLocalMap这个类很关键。

阿里架构师教你如何使用ThreadLocal及原理分析

 

t.threadLocals实际上就是访问Thread类中的ThreadLocalMap这个成员变量

阿里架构师教你如何使用ThreadLocal及原理分析

 

从上面的代码发现每个线程都有本身单独的ThreadLocalMap实例,而对应这个线程的全部本地变量都会保存到这个map内

ThreadLocalMap是在哪里构造?

在 set方法中,有一行代码 createmap(t,value);,这个方法就是用来构造ThreadLocalMap,从传入的参数来看,它的实现逻辑基本也能猜出出几分吧

阿里架构师教你如何使用ThreadLocal及原理分析

 

Threadt 是经过 Thread.currentThread()来获取的表示当前线程,而后直接经过 newThreadLocalMap将当前线程中的 threadLocals作了初始化

ThreadLocalMap是一个静态内部类,内部定义了一个Entry对象用来真正存储数据

阿里架构师教你如何使用ThreadLocal及原理分析

 

分析到这里,基本知道了ThreadLocalMap长啥样了,也知道它是如何构造的?那么我看到这里的时候仍然有疑问

  • Entry集成了 WeakReference,这个表示什么意思?
  • 在构造ThreadLocalMap的时候 newThreadLocalMap(this,firstValue);,key实际上是this,this表示当前对象的引用,在当前的案例中,this指的是 ThreadLocal<Integer>local。那么多个线程对应同一个ThreadLocal实例,怎么对每个ThreadLocal对象作区分呢?

解惑WeakReference

weakReference表示弱引用,在Java中有四种引用类型,强引用、弱引用、软引用、虚引用。

使用弱引用的对象,不会阻止它所指向的对象被垃圾回收器回收。

在Java语言中, 当一个对象o被建立时, 它被放在Heap里. 当GC运行的时候, 若是发现没有任何引用指向o, o就会被回收以腾出内存空间. 也就是说, 一个对象被回收, 必须知足两个条件:

  • 没有任何引用指向它
  • GC被运行.

这段代码中,构造了两个对象a,b,a是对象DemoA的引用,b是对象DemoB的引用,对象DemoB同时还依赖对象DemoA,那么这个时候咱们认为从对象DemoB是能够到达对象DemoA的。这种称为强可达(strongly reachable)

阿里架构师教你如何使用ThreadLocal及原理分析

 

若是咱们增长一行代码来将a对象的引用设置为null,当一个对象再也不被其余对象引用的时候,是会被GC回收的,可是对于这个场景来讲,即时是a=null,也不可能被回收,由于DemoB依赖DemoA,这个时候是可能形成内存泄漏的

阿里架构师教你如何使用ThreadLocal及原理分析

 

经过弱引用,有两个方法能够避免这样的问题

阿里架构师教你如何使用ThreadLocal及原理分析

 

对于方法2来讲,DemoA只是被弱引用依赖,假设垃圾收集器在某个时间点决定一个对象是弱可达的(weakly reachable)(也就是说当前指向它的全都是弱引用),这时垃圾收集器会清除全部指向该对象的弱引用,而后把这个弱可达对象标记为可终结(finalizable)的,这样它随后就会被回收。

试想一下若是这里没有使用弱引用,意味着ThreadLocal的生命周期和线程是强绑定,只要线程没有销毁,那么ThreadLocal一直没法回收。而使用弱引用之后,当ThreadLocal被回收时,因为Entry的key是弱引用,不会影响ThreadLocal的回收防止内存泄漏,同时,在后续的源码分析中会看到,ThreadLocalMap自己的垃圾清理会用到这一个好处,方便对无效的Entry进行回收

解惑ThreadLocalMap以this做为key

在构造ThreadLocalMap时,使用this做为key来存储,那么对于同一个ThreadLocal对象,若是同一个Thread中存储了多个值,是如何来区分存储的呢?

答案就在 firstKey.threadLocalHashCode&(INITIAL_CAPACITY-1)

阿里架构师教你如何使用ThreadLocal及原理分析

 

关键点是 threadLocalHashCode,它至关于一个ThreadLocal的ID,实现的逻辑以下

阿里架构师教你如何使用ThreadLocal及原理分析

 

这里用到了一个很是完美的散列算法,能够简单理解为,对于同一个ThreadLocal下的多个线程来讲,当任意线程调用set方法存入一个数据到Entry中的时候,其实会根据 threadLocalHashCode生成一个惟一的id标识对应这个数据,存储在Entry数据下标中。

  • threadLocalHashCode是经过
  • nextHashCode.getAndAdd(HASH_INCREMENT)来实现的
  • i*HASH_INCREMENT+HASH_INCREMENT,每次新增一个元素(ThreadLocal)到Entry[],都会自增0x61c88647,目的为了让哈希码能均匀的分布在2的N次方的数组里
  • Entry[i]= hashCode & (length-1)

魔数0x61c88647

从上面的分析能够看出,它是在上一个被构造出的ThreadLocal的threadLocalHashCode的基础上加上一个魔数0x61c88647。咱们来作一个实验,看看这个散列算法的运算结果

阿里架构师教你如何使用ThreadLocal及原理分析

 

输出结果

阿里架构师教你如何使用ThreadLocal及原理分析

 

根据运行结果,这个算法在长度为2的N次方的数组上,确实能够完美散列,没有任何冲突, 是否是很神奇。

魔数0x61c88647的选取和斐波那契散列有关,0x61c88647对应的十进制为1640531527。而斐波那契散列的乘数能够用 (long)((1L<<31)*(Math.sqrt(5)-1)); 若是把这个值给转为带符号的int,则会获得-1640531527。也就是说(long)((1L<<31)*(Math.sqrt(5)-1));获得的结果就是1640531527,也就是魔数0x61c88647

阿里架构师教你如何使用ThreadLocal及原理分析

 

总结,咱们用0x61c88647做为魔数累加为每一个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,获得的结果分布很均匀。

图形分析

为了更直观的体现 set方法的实现,经过一个图形表示以下

阿里架构师教你如何使用ThreadLocal及原理分析

 

set剩余源码分析

前面分析了set方法第一次初始化ThreadLocalMap的过程,也对ThreadLocalMap的结构有了一个全面的了解。那么接下来看一下map不为空时的执行逻辑

阿里架构师教你如何使用ThreadLocal及原理分析

 

主要逻辑

  • 根据key的散列哈希计算Entry的数组下标
  • 经过线性探索探测从i开始日后一直遍历到数组的最后一个Entry
  • 若是map中的key和传入的key相等,表示该数据已经存在,直接覆盖
  • 若是map中的key为空,则用新的key、value覆盖,并清理key=null的数据
  • rehash扩容

replaceStaleEntry

因为Entry的key为弱引用,若是key为空,说明ThreadLocal这个对象被GC回收了。 replaceStaleEntry的做用就是把陈旧的Entry进行替换

阿里架构师教你如何使用ThreadLocal及原理分析

 

cleanSomeSlots

这个函数有两处地方会被调用,用于清理无效的Entry

  • 插入的时候可能会被调用
  • 替换无效slot的时候可能会被调用

区别是前者传入的n为元素个数,后者为table的容量

阿里架构师教你如何使用ThreadLocal及原理分析

 

expungeStaleEntry

执行一次全量清理

阿里架构师教你如何使用ThreadLocal及原理分析

 

get操做

set的逻辑分析完成之后,get的源码分析就很简单了

阿里架构师教你如何使用ThreadLocal及原理分析

 

setInitialValue

根据 initialValue()的value初始化ThreadLocalMap

阿里架构师教你如何使用ThreadLocal及原理分析

 

  • 从当前线程中获取ThreadLocalMap,查询当前ThreadLocal变量实例对应的Entry,若是不为null,获取value,返回
  • 若是map为null,即尚未初始化,走初始化方法

remove方法

remove的方法比较简单,从Entry[]中删除指定的key就行

阿里架构师教你如何使用ThreadLocal及原理分析

 

4、ThreadLocal的应用场景及问题

应用场景

ThreadLocal的实际应用场景:

  1. 好比在线程级别,维护session,维护用户登陆信息userID(登录时插入,多个地方获取)
  2. 数据库的连接对象 Connection,能够经过ThreadLocal来作隔离避免线程安全问题

问题

ThreadLocal的内存泄漏

ThreadLocalMap中Entry的key使用的是ThreadLocal的弱引用,若是一个ThreadLocal没有外部强引用,当系统执行GC时,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现一个key为null的Entry,而这个key=null的Entry是没法访问的,当这个线程一直没有结束的话,那么就会存在一条强引用链

阿里架构师教你如何使用ThreadLocal及原理分析

 

Thread Ref - > Thread -> ThreadLocalMap - > Entry -> value 永远没法回收而形成内存泄漏

其实咱们从源码分析能够看到,ThreadLocalMap是作了防御措施的

  • 首先从ThreadLocal的直接索引位置(经过
  • ThreadLocal.threadLocalHashCode & (len-1)运算获得)获取Entry e,若是e不为null而且key相同则返回e
  • 若是e为null或者key不一致则向下一个位置查询,若是下一个位置的key和当前须要查询的key相等,则返回对应的Entry,不然,若是key值为null,则擦除该位置的Entry,不然继续向下一个位置查询

在这个过程当中遇到的key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,天然会被回收。仔细研究代码能够发现,set操做也有相似的思想,将key为null的这些Entry都删除,防止内存泄露。

可是这个设计一来与一个前提条件,就是调用get或者set方法,可是不是全部场景都会知足这个场景的,因此为了不这类的问题,咱们能够在合适的位置手动调用ThreadLocal的remove函数删除不须要的ThreadLocal,防止出现内存泄漏

因此建议的使用方法是

  • 将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,因为一直存在ThreadLocal的强引用,因此ThreadLocal也就不会被回收,也就能保证任什么时候候都能根据ThreadLocal的弱引用访问到Entry的value值,而后remove它,防止内存泄露
  • 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

推荐一个交流学习交流圈子:142019080 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码 分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的 学习资源,目前受益良多

相关文章
相关标签/搜索