ThreadLocal详解

之前在多线程与线程池这篇文章中,给大家简单介绍了一下ThreadLocal这个类,那么在这篇文章中具体细说一下ThreadLocal这个类

       从Java官方文档中的描述:ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。
       我们可以得知 ThreadLocal 的作用是:提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。

总结一下其实就是:

1、线程并发:在多线程并发场景下

2、传递数据:我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量

3、线程隔离:每个线程的变量都是独立的,不会互相影响

常用方法:

使用ThreadLocal方案的好处:

1、传递数据:保存每个线程绑定的数据,在需要的地方可以直接获取,避免参数直接传递带来的代码解耦问题。

2、线程隔离:各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失 

ThreadLocal的内部结构:

       如果我们不去看源代码的话,可能会猜测 ThreadLocal是这样子设计的:每个 ThreadLocal都创建一个Map,然后用线程作为 map 的 key,要存储的局部变量作为 map 的 value,这样就能达到各个线程的局部变量隔离的效果。这是最简单的设计方法,JDK最早期的 ThreadLocal确实是这样设计的,但现在早已不是了。

现在的设计:

JDK后面优化了设计方案,在JDK8中 ThreadLocal的设计是:每个 Thread 维护ThreadLocalMap,这个Map的 key 是 ThreadLocal实例本身,value 才是真正要存储的值 object 。
具体的过程是这样的:

  1. 每个Thread线程内部都有一个Map(ThreadLocalMap)
  2. Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
  3. Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值,
  4. 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离互不干扰。

那么改变之后的好处是什么:

1、每个Map存储的Entry数量变少

2、当Thread销毁的时候,ThreadLocalMap也会随之销毁,减少内存的使用

ThreadLocalMap源码分析:

基本结构:

ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现

(1)成员变量

/**

* 初始容量 -- 必须是2的整次幂

*/

private static final int INITAL_CAPACITY  = 16;

/**

* 存放数据的table,Entry类的定义在下面分析

* 同样,数组长度必须是2的整次幂

*/

private Entry[] table;

/**

* 数组里面entrys的个数,可以用于判断table当前使用量是否超过阈值

*/

private int size = 0;

/**

* 进行扩容的阈值,表使用量大于它的时候进行扩容

*/ 

private int threshold; // Default to 0

跟HashMap类似,INITIAL_CAPACITY代表这个Map的初始容量;table是一个Entry类型的数组,用于存储数据;size代表表中的存储数目;threshold代表需要扩容时对应size的阈值。

(2)存储结构-Entry

/** 

* Entry继承weakReference,并且ThreadLocal作为key.

* 如果key为null(entry.get() == null),意味着key不再被引用,

* 因为这时候entry也可以从table中清除

*/

static class Entry extends weakReference<ThreadLocal<?>> {

    /** The value associated with this ThreadLocal. */

    object value;

    Entry(ThreadLocal<?>k,Object v) {

          super(k);

          value = v;

    }

}

       在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。不过Entry中的key只能是ThreadLocal对象,这点在构造方法中已经限定死了。
       另外,Entry继承WeakReference,也就是key(ThreadLocal)是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑。

弱引用和内存泄漏
有些程序员在使用ThreadLocal的过程中会发现有内存泄漏的情况发生,就猜测这个内存泄漏跟Entry中使用了弱引用的key有关系。这个理解其实是不对的。
我们先来回顾这个问题中涉及的几个名词概念,再来分析问题。
(1)内存泄漏相关概念

  • Memory overflow:内存溢出,没有足够的内存提供申请者使用。
  • Memoryleak: 内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出。

(2)弱引用相关概念
Java中的引用有4种类型:强、软、弱、虚。当前这个问题主要涉及到强引用和弱引用

  • 强引用(“Strong”Reference),就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾回收器就不会回收这种对象。
  • 弱引用(WeakReference),垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

(3)如果key使用强引用
假设ThreadLocalMap中的key使用了强引用,那么会出现内存泄漏吗?
此时ThreadLocal的内存图(实线表示强引用)如下:

1.假设在业务代码中使用完ThreadLocal,threadLocal Ref被回收了。
2.但是因为threadLocalMap的Entry强引用了threadLocal,造成threadLocal无法被回收。
3.在没有手动删除这个Entry以及CurrentThread依然运行的前提下,始终有强引用链 threadRef->currentThread->threadLocalMap->entry,Entry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。
也就是说,ThreadLocalMap中的key使用了强引用,是无法完全避免内存泄漏的。

那么ThreadLocalMap中的key使用了弱引用,会出现内存泄露吗?

1.同样假设在业务代码中使用完ThreadLocal,threadLocal Ref被回收了。
2.由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例,所以threadlocal就可以顺利被gc回收,此时Entry中的key=null。
3.但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry ->value,value不会被回收,而这块value永远不会被访问到了,导致value内存泄漏。
也就是说,ThreadLocalMap中的key使用了弱引用,也有可能内存泄漏。

出现内存泄露的真实原因

比较以上两种情况,我们就会发现,内存泄漏的发生跟ThreadLocalMap中的key是否使用弱引用是没有关系的。
那么内存泄漏的的真正原因是什么呢?
细心的小伙伴会发现,在以上两种内存泄漏的情况中,都有两个前提:
1.没有手动删除这个Entry
2.CurrentThread依然运行

  • 第一点很好理解,只要在使用完ThreadLocal,调用其remove方法删除对应的Entry,就能避免内存泄漏。
  • 第二点稍微复杂一点,由于ThreadLocalMap是Thread的一个属性,被当前线程所引用,所以它的生命周期跟Thread一样长。那么在使用完ThreadLocal的使用,如果当前Thread也随之执行结束,ThreadLocalMap自然也会被gc回收,从根源上避免了内存泄漏。

综上,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏。

那为什么key还要设置为弱引用?

根据刚才的分析,我们知道了:无论ThreadLocalMap中的key使用哪种类型引用都无法完全避免内存泄漏跟使用弱引用没有关系。
要避免内存泄漏有两种方式:

  • 使用完ThreadLocal,调用其remove方法删除对应的Entry
  • 使用完ThreadLocal,当前Thread也随之运行结束

相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的。也就是说,只要记得在使用完ThreadLocal及时的调用remove,无论key是强引用还是弱引用都不会有问题。
那么为什么key要用弱引用呢 ?
事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为nul的话,那么是会对value置为nul的。

这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏

hash冲突的解决

    hash冲突的解决是Map中的一个重要内容。我们以hash冲突的解决为线索,来研究一下ThreadLocalMap的核心源码

首先从ThreadLocal的set()方法入手

public void set(T value) {
          Thread t=Thread.currentThread();
           ThreadLocal.ThreadLocalMap map=getMap(t);
            if(map != null)
            //调用了ThreadLocalMap的set方法
             map.set(this, value);
             else
             createMap(t,value);

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

      }
           void createMap(Thread t,T firstValue) {
           //调用了ThreadLocalmap的构造方法
              t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);

}

这个方法的作用是设置当前线程绑定的局部变量:

A.首先获取当前线程,并根据当前线程获取一个Map
B.如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)
           (这里调用了ThreadLocalMap的set方法)
C.如果Map为空,则给该线程创建 Map,并设置初始值
           (这里调用了ThreadLocalMap的构造方法)

这段代码有两个地方分别涉及到ThreadLocalMap的两个方法,我们接着分析这两个方法。

构造方法'ThreadLocalMap(ThreadLocal<?>firstKey,Object firstValue)'

*firstkey :本ThreadLocal实例(this)
*firstValue :要保存的线程本地变量
ThreadLocalMap(ThreadLocal<?> firstKey,object firstvalue){
//初始化table
table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
//计算索引(重点代码)
int i=firstKey.threadLocalHashCode&(INITIAL_CAPACITY -1);
//设置值
table[i]= new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
size =1;
//设置阈值
setThreshold(INITIAL_CAPACITY);

构造函数首先创建一个长度为16的Entry数组,然后计算出firstKey对应的索引,然后存储到table,并设置size和threshold。

重点分析:int i = firstKey.threadLocalHashCode & (INITAL_CAPACITY -1)。

a. 关于firstKey.threadLocalHashCode;

           private final int threadLocalHashCode nextHashCode();


                      private static int nextHashcode(){
                      return nextHashCode.getAndAdd(HASH_INCREMENT);

          }
//AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减,适合高并发情况下的使用
               private static AtomicInteger nextHashCode = new AtomicInteger();
               //特殊的hash值
                private static final int HASH_INCREMENT = 0x61c88647;

     这里定义了一个AtomicInteger类型,每次获取当前值并加上HASH_INCREMENT,HASH_INCREMENT =0x61c88647,这个值跟斐波那契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里,也就是Entry0table中,这样做可以尽量避免hash冲突。


b.关于&(INITIAL_CAPACITY-1)
       计算hash的时候里面采用了hashCode &(size-1)的算法,这相当于取模运算hashCode % size的一个更高效的实现。正是因为这种算法,我们要求size必须是2的整次幂,这也能保证保证在索引不越界的前提下,使得hash发生冲突的次数减小。

重点分析:

        重点分析 :ThreadLocalMap使用 线性探测法 来解决哈希冲突的。
该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。
        举个例子,假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。
        按照上面的描述,可以把Entry0 table看成一个环形数组。

感谢大家的支持,后续会持续更新!!!

  • 22
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值