全面解析ThreadLocal

1.ThreadLocal概述

咱们先不看官方是如何说明ThreadLocal的,先做个名词拆解

  • Thread:线程
  • Local:局部的
  • ThreadLocal:线程局部的

官方解释如下

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).
 */

大致意思就是ThreadLocal提供了线程局部变量,这些变量不同之处在于,每一个线程读取的变量都是一个变量的副本,这些变量在线程间是相互隔离的,通过get和set方法就可以得到当前线程对应的值。

是不是不太明白,找一个我们熟悉的对比着说一下,我们都知道普通方法内的局部变量,这个局部变量的生命周期就在方法体内,方法执行结束,变量就失效了,ThreadLocal提供的是生命周期和线程一样的长的线程局部变量,线程局部变量属于线程私有。

基本使用

常用方法

​ 在使用之前,我们先来认识几个ThreadLocal的常用方法

方法声明描述
ThreadLocal()创建ThreadLocal对象
public void set( T value)设置当前线程绑定的局部变量
public T get()获取当前线程绑定的局部变量
public void remove()移除当前线程绑定的局部变量
代码演示
public class ThreadLocalDemo01 {
    //ThreadLocal变量t1是String类型
    public static final ThreadLocal<String> t1 = new ThreadLocal();
    //ThreadLocal变量t2是String类型
    public static final ThreadLocal<String> t2 = new ThreadLocal();

    public static void main(String[] args) {
        //线程1
        Thread thread_01=new Thread(new Runnable() {
            @Override
            public void run() {
                t1.set("Thread_01 String01");
                t2.set("Thread_01 String02");
                String str01 = t1.get();
                String str02 = t2.get();
                System.out.println(str01);
                System.out.println(str02);
            }
        },"Thread_01");

        //线程2
        Thread thread_02=new Thread(new Runnable() {
            @Override
            public void run() {
                t1.set("Thread_02 String01");
                t2.set("Thread_02 String02");
                String str01 = t1.get();
                String str02 = t2.get();
                System.out.println(str01);
                System.out.println(str02);
            }
        },"Thread_02");

        thread_01.start();
        thread_02.start();
        //主线程
        String str01=t1.get();
        String str02=t1.get();
        System.out.println(Thread.currentThread().getName()+"==>"+str01);
        System.out.println(Thread.currentThread().getName()+"==>"+str02);
    }
}
  • 运行结果
main==>null
Thread_02 String01
Thread_02 String02
Thread_01 String01
Thread_01 String02
main==>null
  • 分析
从结果可以看出,我们通过set方法在不同的线程赋给ThreadLocal实例不同的值,得到的结果也不一样,
我们在main线程中没有给t1和t2赋值,返回的结果就是null

这就是所谓的,通过ThreadLocal可以提供线程私有的变量,这些变量都是一个变量的副本

2.ThreadLocal的内部结构

现在我们一起来看一下ThreadLocal的内部结构,探究它能够实现线程数据隔离的原理。

原理探究

在JDK8中ThreadLocal的设计是:每个Thread维护一个ThreadLocalMap,这个Map的key是ThreadLocal实例本身,value才是真正要存储的值Object。

具体的过程是这样的:

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

在这里插入图片描述
这样设计的好处:

  • ThreadLocalMap中存储数量由ThreadLocal的数量决定。
  • 当Thread销毁之后,对应的ThreadLocalMap也会随之销毁(对ThreadLocalMap的引用在Thread中),能减少内存的使用。

3.ThreadLocal的核心方法源码

如果看过线程类的源码的同学可能知道,在Thread类中有一个threadlocals的变量,官方的解释中说,ThreadLocal值是与此线程相关的,这个map由ThreadLocal类维护。

Q: 为什么这个变量是threadLocals,不是threadlocal
A:因为一个线程中可能会有多个TheadLocal,实际上真正维护这个threadLocals变量的是ThreadLocal的静
  态内部类ThreadLocalMap,ThreadLocalMap为每个Thread都维护了一个数组table,真正存储
  ThreadLocal的是这个map类型的table数组,大家知道map中存储的是一个一个键值对(Entry),通过
  ThreadLocal变量实例就是key的值,value存储的就是这个实例对应的值。
/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

每个线程持有一个 threadLocals变量,也就是一个ThreadLocalMap对象,即每一个新的线程Thread都会实例化一个ThreadLocalMap并赋值给成员变量threadLocals,使用时若已经存在threadLocals则直接使用已经存在的对象

下面就通过源码来看看具体的实现原理:

set方法

  • ThreadLocal的set方法
/**
 * 设置当前线程对应的ThreadLocal的值
 * @param value 前当线程中ThreadLocal对用的value
 */
public void set(T value) {
    // 获取当前线程对象
    Thread t = Thread.currentThread();
    // 获取此线程对象中维护的ThreadLocalMap对象,后面会分析这个getMap()方法
    ThreadLocalMap map = getMap(t);
    // 判断map是否存在
    if (map != null)
        // 存在则调用map.set设置此实体entry,这个是ThreadLocalMap类中set的方法,后面会分析
        map.set(this, value);
    else
        // 当前线程Thread 不存在ThreadLocalMap对象
        // 则调用createMap进行ThreadLocalMap对象的初始化
        // 并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
        //后面也会分析这个createMap()方法
        createMap(t, value);
}
  • ThreadLocal的getMap方法
/**
 * 获取当前线程Thread对应维护的ThreadLocalMap 
 * 可以看出这个变量是threadLocals,该变量定义在Thread类中,是ThreadLocalMap类型的
 * 每个Thread实例中都包含这个threadLocals变量
 * 
 * @param  t 当前线程
 * @return the map 对应维护的ThreadLocalMap 
 */
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
  • ThreadLocal的createMap方法
/**
 * 创建当前线程Thread对应维护的ThreadLocalMap 
 * 在Thread类中虽然定义了ThreadLocalMap类型threadLocals变量,但是并没有初始化
 * 如果第一次存储ThreadLocal变量要进行初始化,和HashMap的延迟初始化一样
 * 
 * @param t 当前线程
 * @param firstValue 存放到map中第一个entry的值
 */
void createMap(Thread t, T firstValue) {
    //这里的this是调用此方法的threadLocal
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

代码执行流程:

  • A. 首先获取当前线程,并根据当前线程获取一个ThreadLocalMap
  • B. 如果获取的ThreadLocalMap不为空,则将参数设置到ThreadLocalMap中(当前ThreadLocal的引用作为key)
  • C. 如果hreadLocalMap为空,则给该线程创建hreadLocalMap,并设置初始值

总结

  • 通过上面的这些方法的分析,我们要知道,对这个ThreadLocalMap是的引用是定义在Thread类中的threadLocals变量,threadLocals变量是Thread实例私有的,也就是每个线程都会创建一个ThreadLocalMap,这就说明了为什么是线程隔离的。
  • ThreadLocalMap的key是threadLocals实例,value是我们ThreadLocal泛型中对应类型的值,
  • 我们通过:Thread实例-->ThreadLocalMap-->ThreadLocal对应的值

get方法

  • ThreadLocal的get方法
/**
* 返回当前线程中保存ThreadLocal的值
* 如果当前线程没有此ThreadLocal变量,
* 则它会通过调用setInitialValue()方法进行初始化值
* 	Q:如果没有给对应的ThreadLocal赋值,如何初始化值,这个值是什么,
* 	A:我们待会会分析这个setInitialValue()方法
* 
* @return 返回当前线程对应此ThreadLocal的值
*/
public T get() {
   // 获取当前线程对象
   Thread t = Thread.currentThread();
   // 获取此线程对象中维护的ThreadLocalMap对象
   ThreadLocalMap map = getMap(t);
   // 如果此map存在
   if (map != null) {
       // 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
       //这个getEntry方法是ThreadLocalMap中的方法,后面会做分析
       ThreadLocalMap.Entry e = map.getEntry(this);
       // 对e进行判空 
       if (e != null) {
           @SuppressWarnings("unchecked")
           // 获取存储实体 e 对应的 value值
           // 即为我们想要的当前线程对应此ThreadLocal的值
           T result = (T)e.value;
           return result;
       }
   }
   /*
   	初始化 : 有两种情况有执行当前代码
   	第一种情况: map不存在,表示此线程没有维护的ThreadLocalMap对象
   	第二种情况: map存在, 但是没有与当前ThreadLocal关联的entry
    */
   return setInitialValue();
}
  • ThreadLocal的setInitialValue方法
/**
* 初始化
*
* @return the initial value 初始化后的值
*/
private T setInitialValue() {
   // 调用initialValue获取初始化的值
   // 此方法可以被子类重写, 如果不重写默认返回null,后面我们会分析这个方法
   T value = initialValue();
   // 获取当前线程对象
   Thread t = Thread.currentThread();
   // 获取此线程对象中维护的ThreadLocalMap对象
   ThreadLocalMap map = getMap(t);
   // 判断map是否存在
   if (map != null)
       // 存在则调用map.set设置此实体entry
       //这个set是ThreadLocalMap中的方法,后面会做分析
       map.set(this, value);
   else
       // 当前线程Thread 不存在ThreadLocalMap对象
       // 则调用createMap进行ThreadLocalMap对象的初始化
       // 并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
       // 注意:不重写initialValue()方法,这个值就是null,上面问题的答案就在这里
       createMap(t, value);
   // 返回设置的值value
   return value;
}

代码执行流程

  • A. 首先获取当前线程, 根据当前线程获取一个ThreadLocalMap
  • B. 如果获取的ThreadLocalMap不为空,则在ThreadLocalMap中以ThreadLocal的引用作为key,在ThreadLocalMap中获取对应的Entry e,否则转到D
  • C. 如果e不为null,则返回e.value,否则转到D
  • ​D. ThreadLocalMap为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的ThreadLocalMap

总结:

  • 先获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始值。

remove方法

  • ThreadLocal的remove方法
/**
* 删除当前线程中保存的ThreadLocal对应的实体entry
*/
public void remove() {
  // 获取当前线程对象中维护的ThreadLocalMap对象
   ThreadLocalMap m = getMap(Thread.currentThread());
  // 如果此map存在
   if (m != null)
      // 存在则调用map.remove
      // 以当前ThreadLocal为key删除对应的实体entry
      //这个remove是ThreadLcoalMap中的方法,后面会做分析
      m.remove(this);
}

initialValue方法

  • ThreadLocal中的initialValue方法:
/**
 * 返回当前线程对应的ThreadLocal的初始值
 *  
 * 此方法的第一次调用发生在,当线程没有通过set方法存储此ThreadLocal值就通过get方法访问此线程的
 * ThreadLocal值,在这种情况下,initialValue 才会被这个线程调用。
 * 
 * 通常情况下,每个线程最多调用一次这个方法。
 *
 * 这个方法仅仅简单的返回null 
 * 如果程序员想ThreadLocal线程局部变量有一个除null以外的初始值,
 * 必须通过子类继承ThreadLocal的方式去重写此方法,通常, 可以通过匿名内部类的方式实现
 *
 * @return 当前ThreadLocal的初始值
 */
protected T initialValue() {
   return null;
}

4.ThreadLocalMap源码分析

ThreadLocalMap的源码相对比较复杂, 我们从以下三个方面进行讨论。

基本结构

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

至于为什么不实现Map接口,后面会解释

在这里插入图片描述

成员变量

这个和HashMap差不多:

  • INITIAL_CAPACITY:代表这个Map的初始容量
  • table :是一个Entry 类型的数组,用于存储数据
  • size :代表表中的存储数目
  • threshold: 代表需要扩容时对应 size 的阈值。
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;

/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;

/**
* The number of entries in the table.
*/
private int size = 0;

/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0

ThreadLocal.createMap方法

为什么要在这个地方在说一下这个ThreadLocal.createMap方法那,因为在这个方法的内部调用了ThreadLcoalMap的构造方法:

void createMap(Thread t, T firstValue) {
    //这里的this是调用此方法的threadLocal
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocal中有两个方法调用了这个方法:

  • 第一个就是ThreadLocal.set方法

在set方法中:当第一次set值的时候,创建ThreadLocalMap

  • 第二个就是ThreadLocal.get方法中调用的setInitialValue方法

在还没有set就get值的时候,创建ThreadLocalMap

存储结构 - Entry

Entry是ThreadLocalMap的一个静态内部类:

ThreadLocalMap中存储的就是Entry就是用这个类封装的,不是Map接口中的Entry

/*
 * Entry继承WeakReference,并且用ThreadLocal作为key.
 */
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:内存溢出,没有足够的内存提供申请者使用。

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

  • 2、 弱引用相关概念

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

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

弱引用(WeakReference),垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

  • 3、如果key使用强引用

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

此时ThreadLocal的内存图(实线表示强引用)如下:
在这里插入图片描述

假设在业务代码中ThreadLocal使用完了,threadLocal Ref对threadLocal的引用就断开了,此时,
threadlocal实例就应该被回收,就好比一个类中某方法的局部变量,在该方法执行完之后就应该被释放。

如果当前线程没有执行完毕,该线程的threadLocalMap就不会被回收,而里面存储的Entry如果强引用了
threadLocal,就算threadLocal Ref对threadLocal的引用就断开了,Entry对hreadLocal的引用还是
会造成threadLocal无法被回收。

在没有手动删除这个Entry以及当前线程依然运行的前提下,Entry就不会被回收(Entry中包括了ThreadLocal实例和value),Entry导致内存泄漏。

也就是说,ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的,如果不使用remove方法
手动删除,就会导致内存泄露
  • 4、如果key使用弱引用

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

此时ThreadLocal的内存图(实线表示强引用,虚线表示弱引用)如下:
在这里插入图片描述

假设在业务代码中ThreadLocal使用完了,threadLocal Ref对threadLocal的引用就断开了,此时,
threadlocal实例就应该被回收,就好比一个类中某方法的局部变量,在该方法执行完之后就应该被释放。

如果当前线程没有执行完毕,该线程的threadLocalMap就不会被回收,而里面存储的Entry如果弱引用了
threadLocal,当threadLocal Ref对threadLocal的强引用断开之后,由于ThreadLocalMap只持有
ThreadLocal的弱引用,没有任何强引用指向threadlocal实例,,所以threadlocal就可以顺利被gc回收,
此时Entry中的key=null。

​但是在没有手动删除这个Entry以当前线程(CurrentThread)依然运行的前提下,也存在有强引用链 
CurrentThread Ref->currentThread->threadLocalMap->entry -> value ,value不会被回收, 
而这块value永远不会被访问到了,导致value内存泄漏。
也就是说,ThreadLocalMap中的key使用了弱引用,也有可能内存泄漏。
  • 5、出现内存泄漏的真实原因

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

细心的同学会发现,在以上两种内存泄漏的情况中,都有两个前提:

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

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

要避免内存泄漏有两种方式:

  • 使用完ThreadLocal,调用其remove方法删除对应的Entry
  • 使用完ThreadLocal,当前Thread也随之运行结束
  • 相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的。
  • 也就是说,只要记得在使用完ThreadLocal及时的调用remove,无论key是强引用还是弱引用都不会有问题。

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

  • 事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。
  • ​ 这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障,弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。

hash冲突的解决

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

注意:HashMap和ConCurrentHashMap中解决hash冲突的方式是使用链表或者红黑树

ThreadLocaMap的构造方法
  • ThreadLocal.set()方法,这个方法我们前面已经分析过了,我们用它引出ThreadLocaMap的构造方法
 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);
   }
  • 构造方法: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。

注意:这个和HashMap不一样,不能设置初始值,初始值大小是16
  • 重点分析: int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)

1、 firstKey.threadLocalHashCode

//firstKey是ThreadLcoal的实例,调用ThreadLcoal内部的变量threadLocalHashCode
//这个变量的值,是通过nextHashCode()拿到的
private final int threadLocalHashCode = nextHashCode();

//ThreadLocal.nextHashCode()方法
private static int nextHashCode() {
	//nextHashCode这个值调用getAndAdd()方法
	//就是:nextHashCode=nextHashCode+HASH_INCREMENT
	//nextHashCode值是哪里来的
   return nextHashCode.getAndAdd(HASH_INCREMENT);
}

//AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减,适合高并发情况下的使用
//nextHashCode是通过这个原子性的Integer类new出来的
//注意这个值是在ThreadLocal类中的,也就是每一个ThreadLcoal都会有这个属性
//nextHashCode=0,空参构造默认为0
private static AtomicInteger nextHashCode =  new AtomicInteger();
//特殊的hash值
//为什么要加上这个值那?  记住就行,不必深究
// 因为这样会减少hash冲突
private static final int HASH_INCREMENT = 0x61c88647;

​ HASH_INCREMENT = 0x61c88647,这个值跟斐波那契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里, 也就是Entry[] table中,这样做可以尽量避免hash冲突。

2、& (INITIAL_CAPACITY - 1)

计算hash的时候里面采用了hashCode & (size - 1)的算法,这相当于取模运算hashCode % size的一个更
高效的实现。正是因为这种算法,我们要求size必须是2的整次幂,这也能保证在索引不越界的前提下,使得
hash发生冲突的次数减小。
ThreadLocalMap.set方法:(重点)
private void set(ThreadLocal<?> key, Object value) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        //计算索引(重点代码,刚才分析过了)
        int i = key.threadLocalHashCode & (len-1);
        /**
         * 使用线性探测法查找元素(重点代码)
         */
        for (ThreadLocal.ThreadLocalMap.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 是一个陈旧(stale)的元素
            if (k == null) {
                //用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏
                replaceStaleEntry(key, value, i);
                return;
            }
        }
    
    	    //ThreadLocal对应的key不存在并且没有找到陈旧的元素,则在空元素的位置创建一个新的Entry。
            tab[i] = new Entry(key, value);
            //sz加1
            int sz = ++size;
            /**
             * cleanSomeSlots用于清除那些e.get()==null的元素,
             * 这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置null。
             * 如果没有清除任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行
             * rehash(执行一次全表的扫描清理工作)
             * 
             * 也就是每次添加完之后清除一下,再看看是否超出阈值,超出就调用rehash()方法扩容
             */
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
}

/**
 * 获取环形数组的下一个索引
 */
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

​ 代码执行流程:

  • A. 首先还是根据key计算出索引 i,然后查找i位置上的Entry,
  • B. 若是Entry已经存在并且key等于传入的key,那么这时候直接给这个Entry赋新的value值,
  • C. 若是Entry存在,但是key为null,则调用replaceStaleEntry来更换这个key为空的Entry,
  • D. 不断循环检测,直到遇到为null的地方,这时候要是还没在循环过程中return,那么就在这个null的位置新建一个Entry,并且插入,同时size增加1。
  • E.最后调用cleanSomeSlots,清理key为null的Entry,最后返回是否清理了Entry,接下来再判断sz 是否>= thresgold达到了rehash的条件,达到的话就会调用rehash函数执行一次全表的扫描清理。

重点分析 : ThreadLocalMap使用线性探测法来解决哈希冲突的

  • 该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出

  • 举个例子,假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。

  • 按照上面的描述,可以把Entry[] table看成一个环形数组

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

彤彤的小跟班

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值