ThreadLocal初探

一、ThreadLocal介绍

一、官方介绍

从Java官方文档中的描述:ThreadLocal类用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过get和set方法访问)时,能够保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static 类型的,用来关联线程个线程上下文。

 二、ThreadLocal线程的作用

提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量的复杂度。

总结:

1、线程并发:ThreadLocal一般是在多线程并发的场景下使用,不是多线程的环境下是没必要使用的。

2、传递数据:在同一个线程,不同的组件进行公共变量的传递可以使用ThreadLocal

3、线程隔离:每一个线程中的变量都是独立的,线程之间的变量互不影响。

三、常用方法

四、 ThreadLocal与Synchronized之间的区别

虽然两者都是用于处理多线程并发访问变量的问题,不过两者之间处理问题的角度和思路不同。

二、ThreadLocal的内部结构

一、ThreadLocal常见误解

ThreadLocal早期设计:每一个ThreadLocal都会创建一个map,然后用线程作为map的key,要存储的局部变量作为map的value,这样就能达到各个线程的局部变量隔离的效果。

二、 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、对于不同线程,每次获取副本值时,别的线程并不能获取当前线程的副本值,形成了副本的隔离,互不干扰。

三、两者对比

 三、ThreadLocal源码分析

ThreadLocal对外暴露的方法有以下几个:

 一、Set方法

1、首先获取当前线程,并根据当前线程获取一个Map集合

2、如果获取的集合不为空,则将参数设置到map中,其中ThreadLocal作为key

3、如果为空,则给改线程创建一个新的map,并设置初始值。

 二、get方法

/**
 返回该线程本地变量当前线程的副本中的值。
 如果变量没有当前线程的值,则首先将其初始化为调用 {@ link initialValue} 方法返回的值。
 返回该线程的当前线程值-local
 */
public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程维护的ThreadLocalMap 
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 以当前ThreadLocal为key,获取对应的存储实体。this表示当前对象ThreadLocal
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
             // 这里主要是想获取当前线程,对应的ThreadLocal的值
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    /*
     初始化:有两种情况下会执行
        1、map不存在时会执行,表示当前线程没有维护ThreadLocalMap 
        2、map存在,但是没有与当前ThreadLocal关联的值
     */
    return setInitialValue();
}

 

三、remove方法

 四、initialValue方法

 五、ThreadLocalMap源码分析

一、基本结构

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

二、ThreadLocalMap中的成员变量 

 三、存储结构--Entry

 四、内存泄漏与弱引用

(1)内存泄漏相关概念:

1、Memery overflow:内存溢出,没有足够的内存给申请者使用。

2、Memery leak:内存泄漏,程序中已动态分配的堆内存,由于某种原因程序未能释放或无法释放,造成系统内存的浪费,导致程序运行减慢或者系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出。

(2)弱引用相关概念

java中的引用有四种类型:强、软、弱、虚。

1、强引用(Strong Reference):就是我们最常见的普通对象的应用,只要还有强应用指向一个对象,就表明对象还活着,垃圾收集器就不会回收这个对象的内存。

2、软引用(Soft Reference ):

如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它;

如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

3、弱引用(Weak Reference ):垃圾收集器一旦发现有弱引用的对象,不管当前内存是否足够,都会立即回收他的内存。

4、虚引用(Phantom Reference):

虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。

要注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

之后会另起一篇记录

 (3)如果key是强引用

假设ThreadLocalMap中的key使用了强引用, 那么会出现内存泄漏吗?

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

也就是说: ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的
 

(4)如果key是弱引用 

1、假设在业务代码中使用完ThreadLocal, ThreadLocal ref被回收了

2、由于threadLocalMap只持有ThreadLocal的弱引用, 没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收, 此时Entry中的key = null

3、在没有手动删除Entry以及CurrentThread依然运行的前提下, 也存在有强引用链threadRef → currentThread → value, value就不会被回收, 而这块value永远不会被访问到了, 导致value内存泄漏

也就是说: ThreadLocalMap中的key使用了弱引用, 也有可能内存泄漏。
 

(5)内存泄漏的真实原因 

出现内存泄漏的真实原因出改以上两种情况,
比较以上两种情况,我们就会发现:
内存泄漏的发生跟 ThreadLocalIMap 中的 key 是否使用弱引用是没有关系的。那么内存泄漏的的真正原因是什么呢?

在以上两种内存泄漏的情况中.都有两个前提:
1 、没有手动删除这个 Entry
2 ·、CurrentThread 依然运行
第一点很好理解,只要在使用完下 ThreadLocal ,调用其 remove 方法翻除对应的 Entry ,就能避免内存泄漏。
第二点,由于ThreodLocalMap 是 Thread 的一个属性,被当前线程所引所以它的生命周期跟 Thread 一样长。那么在使用完 ThreadLocal 的使用,如果当前Thread 也随之执行结束, ThreadLocalMap 自然也会被 gc 回收,从根源上避免了内存泄漏。

综上, ThreadLocal 内存泄漏的根源是:

由于ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏.
 

(6)为什么使用弱引用 

无论 ThreadLocalMap 中的 key 使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。

​ 要避免内存泄漏有两种方式:
​ 1 .使用完 ThreadLocal ,调用其 remove 方法删除对应的 Entry
​ 2 .使用完 ThreadLocal ,当前 Thread 也随之运行结束

​ 相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的.

​ 也就是说,只要记得在使用完ThreadLocal 及时的调用 remove ,无论 key 是强引用还是弱引用都不会有问题.
 

那么为什么 key 要用弱引用呢

​ 事实上,在 ThreadLocalMap 中的set/getEntry 方法中,会对 key 为 null (也即是 ThreadLocal 为 null )进行判断,如果为 null 的话,那么是会又如 value 置为 null 的.

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

五、Hash冲突解决

Hash冲突的解决是Map中的一个重要内容。

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

/**
 * firstKey:ThreadLocal实例
 * firstValue:要保存的线程本地变量
 */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 为Entry数组设置初始化值16
    table = new Entry[INITIAL_CAPACITY];
    // 计算索引
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    // 设置值
    table[i] = new Entry(firstKey, firstValue);
    // 数组存储的个数
    size = 1;
    // 设置阈值
    setThreshold(INITIAL_CAPACITY);
}

构造函数首先创建一个长度为16的数组,然后计算出firstKey对应的索引,然后存储到table

中,并设置size和threshould

主要分析: int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

1、firstKey.threadLocalHashCode

private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private static AtomicInteger nextHashCode =
    new AtomicInteger()
private static final int HASH_INCREMENT = 0x61c88647;

 &(INITIAL_CAPACITY - 1)

2、ThreadLocalMap中的Set方法

private void set(ThreadLocal<?> key, Object value) {

    // We don'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 = table;
    int len = tab.length;
    // 计算索引,相当于对数组长度取余
    int i = key.threadLocalHashCode & (len-1);

    /**
        使用线性探测法查找元素
        Entry e = tab[i]:取出i这个位置的Entry 
        e != null:对取出的Entry 进行判空,不为空时继续循环
        e = tab[i = nextIndex(i, len)]:长度加一
    */
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        // ThreadLocal对应的key存在,则直接覆盖之前的值
        if (k == key) {
            e.value = value;
            return;
        }
        // key为null,但是值不为null,说明之前的ThreadLocal对象已经被回收了,当前       
        // Entry是一个陈旧元素
        if (k == null) {
                // 用新元素替换旧元素,并且进行垃圾清理,防止内存泄漏
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    /**
        ThreadLocal对应的key不存在,且没有找到陈旧元素,则在空元素的位置创建一个新的             Entry
     */   
    tab[i] = new Entry(key, value);
    int sz = ++size;
     
     /**
        cleanSomeSlots用于清除e.get()==null的元素
        这种数据key关联的对象已经被回收了,如果没有
        清理任何的Entry,并且当前数量达到了负载因子所定义(长度的三分之二)
        那么进行rehash();执行一次全表的扫描清理工作
     **/   
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

// 获取环形数组的下一个索引

private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值