ThreadLocal的原理分析以及ThreadLocalMap的源码分析

目录

ThreadLocal的简单使用

 ThreadLocal 的数据结构

ThreadLocal的核心方法介绍

set

get

remove

ThreadLocalMap源码分析

ThreadLocalMap的内存泄漏问题

构造函数

getEntry方法

 set方法


 

  1. 什么是ThreadLocal?
    ThreadLocal类顾名思义可以理解为线程本地变量。也就是说如果定义了一个ThreadLocal,每个线程往这个ThreadLocal中读写是线程隔离,互相之间不会影响的。它提供了一种将可变数据通过每个线程有自己的独立副本从而实现线程封闭的机制。

  2. 它大致的实现思路是怎样的?
    Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。每个线程在往某个ThreadLocal里塞值的时候,都会往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。

我们可以得知ThreadLocal 的作用是:提供线程内的局部变量,不同线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量传递的复杂度。

一句话就是:线程并发下的数据隔离以及数据传递

ThreadLocal的简单使用

先来看下简单的使用,比如下面的demo,我们开启五个线程,分别去设置每个线程自己的content ,再get

package com.cjian.threadlocal;

/**
 * 需求:线程隔离
 * 在多线程并发的场景下,每个线程中的变量都是相互独立的
 * @description:
 * @author: CJ
 * @time: 2020/11/13 16:34
 */
public class ThreadLocalDemo {

    private String content;

    public String getContent() {
        return content;
    }

    public void  setContent(String content) {
         this.content = content;
    }

    public static void main(String[] args) {
        ThreadLocalDemo demo = new ThreadLocalDemo();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                    demo.setContent(Thread.currentThread().getName() + "的数据");
                    System.out.println(Thread.currentThread().getName() + "---->" + demo.getContent());
            }).start();
        }
    }
}

输出为:

ThreadLocal的原理分析以及ThreadLocalMap的源码分析_第1张图片

多运行几次每次的结果会不相同,但是有一个共同点->不同线程之间的content乱了,如果想实现每个线程准确的获取自己设置的content值,应该怎么做呢?

我们首先想到的就是使用synchronized关键字,我没来试一下:

package com.cjian.threadlocal;

public class ThreadLocalDemo {

    private String content;

    public String getContent() {
        return content;

    public void  setContent(String content) {
         this.content = content;
    }

    public static void main(String[] args) {
        ThreadLocalDemo demo = new ThreadLocalDemo();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                synchronized (demo) {
                    demo.setContent(Thread.currentThread().getName() + "的数据");
                    System.out.println(Thread.currentThread().getName() + "---->" + demo.getContent());
                }
            }).start();
        }
    }
}

结果如我们所愿:

ThreadLocal的原理分析以及ThreadLocalMap的源码分析_第2张图片

但是呢,问题来了,在高并发的情况下使用synchronized会导致程序的并发性降低,那还有其他办法吗?下面就是主角了:使用 ThreadLocal

我们 先来实现以下上面的demo:

package com.cjian.threadlocal;

public class ThreadLocalDemo {
    ThreadLocal tl = new ThreadLocal<>();

    private String content;

    public String getContent() {
        return tl.get();
    }

    public void  setContent(String content) {
        tl.set(content);
    }

    public static void main(String[] args) {
        ThreadLocalDemo demo = new ThreadLocalDemo();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                    demo.setContent(Thread.currentThread().getName() + "的数据");
                    System.out.println(Thread.currentThread().getName() + "---->" + demo.getContent());
            }).start();
        }
    }
}

运行结果:

ThreadLocal的原理分析以及ThreadLocalMap的源码分析_第3张图片

虽然ThreadLocal 和synchronized都能解决多线程并发访问变量的问题,但两者是有区别的:

  synchronized ThreadLocal
原理 同步机制采用‘时间换空间’的的方式,只提供一份变量,让不同的线程排队访问 采用‘空间换时间’的方式,为每一个线程都提供了一份变量副本,从而实现同时访问时而互不干扰
侧重点 多个线程之间访问共享资源的同步 多线程中让每个线程之间的数据相互隔离

那么ThreadLocal 底层原理是什么呢?

 ThreadLocal 的数据结构

如果我们不看源码的话,我们可能会猜测每个ThreadLocal都会创建一个Map,然后用线程作为map的key,要存储的局部变量作为value,JDK早期确实是这样设计的,但现在早不是了。

JDK1.8中:每个ThreadLocal 维护一个ThreadLocalMap,这个map的key是ThreadLocal实例本身,value是要存储的值:

  1. 每个Thread线程内部都有一个ThreadLocalMap;
  2. ThreadLocalMap里面存储ThreadLocal对象(key)和线程的变量副本(value)
  3. ThreadLocal内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值

ThreadLocal的原理分析以及ThreadLocalMap的源码分析_第4张图片

这样设计的好处:

每个Map存储的Entry减少;当Thread销毁的时候ThreadLocalMap也会随之销毁,减少内存的使用


我们继续分析ThreadLocal的源码:

主要由下面四个方法:

  1. protected T initialValue() :返回当前线程局部变量的初始值
  2. public void set(T value):设置当前线程绑定的局部变量
  3. public T get():获取当前线程绑定的局部变量
  4. public void remove():移除当前线程绑定的局部变量

ThreadLocal的核心方法介绍

set

//设置当前线程对应的ThreadLocal的值
public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取此线程中维护的ThreadLocalMap 
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //如果mapo不为空,设置值,注意this为当前调用set方法的threadlocal对象
            map.set(this, value);
        else
            //当前线程不存在ThreadLocalMap ,则调用createMap进行ThreadLocalMap 的初始化
            //并将当前线程t和value作为第一个Entry存放至ThreadLocalMap 中
            createMap(t, value);
    }

//获取当前线程Thread对应的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

总结一下set方法:

  1. 首先获取当前线程,并根据当前线程获取到该线程维护的ThreadLocalMap
  2. 如果map不为空,则以threadlocal为空,将value值set进map
  3. 如果map为空,则给该线程创建map,并设置初始值

get

 public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //以当前threadlocal为key,获取对应的存储实体Entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                //如果e不为空,获取存储的value
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //初始化:有2种情况
        //1)map不存在,表示此线程没有维护的ThreadLocalMap
        //2)map存在,但是没有与当前threadlocal关联的entry
        return setInitialValue();
    }

//初始化
private T setInitialValue() {
        //调用该方法获取初始化的值
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //存在则设置值
            map.set(this, value);
        else
            //当前线程不存在ThreadLocalMap,则对ThreadLocalMap 进行初始化,set方法中有分析
            createMap(t, value);
        //返回设置的value
        return value;
    }

//此方法可以被子类重写,通常采用匿名内部内的方式实现。
//返回当前线程对应的threadlocal的初始值。
//此方法的第一次调用发生在当线程通过get方法访问此线程的threadlocal值时,
//且当前线程未调用set方法,通常情况下,每个线程最多调用一次该方法
 protected T initialValue() {
        return null;
    }

通过对get方法的分析可得到:如果在set之前执行get方法,则会返回默认值null,如果想改变该默认值,可通过子类去重写

总结一下get方法:

  1. 首先获取当前线程,并根据当前线程获取到该线程维护的ThreadLocalMap
  2. 如果map不为空,则在map中以threadlocal为key获取对应的entry,map如果为空,则4
  3. 如果entry不为空,则返回entry.value,否则4
  4. map为空或者entry为空,则通过initialValue获取最初的默认值value,然后使用threadlocal和value作为第一个entry去创建map

remove

//删除当前线程中保存的threadlocal对应的实体entry
public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             //以当前threadlocal为空删除对应的实体entry
             m.remove(this);
     }

ThreadLocalMap源码分析

先分析一些简单的方法和属性: 

static class ThreadLocalMap {
 
    /**    
         * 自定义一个Entry类,并继承自弱引用
         * 用来保存ThreadLocal和Value之间的对应关系
         *
         * 之所以用弱引用,是为了解决线程与ThreadLocal之间的强绑定关系
         * 会导致如果线程没有被回收,则GC便一直无法回收这部分内容
         */
    static class Entry extends WeakReference> {
        Object value;
 
        Entry(ThreadLocal k, Object v) {
            super(k);
            value = v;
        }
    }
    
    /**
         * The initial capacity -- MUST be a power of two.
         * Entry数组的初始化大小,必须是2的幂
         */
    private static final int INITIAL_CAPACITY = 16;
 
    /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         * 数组
         * 长度必须是2的N次幂
         * 这个可以参考为什么HashMap里维护的数组也必须是2的N次幂
         * 主要是为了减少碰撞,能够让保存的元素尽量的分散
         * 关键代码还是hashcode & table.length - 1  前面的博文都有分析
         */
    private Entry[] table;
 
    /**
         * The number of entries in the table.
         * table里的元素个数
         */
    private int size = 0;
 
    /**
         * The next size value at which to resize.
         * 扩容的阈值
         */
    private int threshold; // Default to 0
 
    /**
         * Set the resize threshold to maintain at worst a 2/3 load factor.
         * 根据长度计算扩容的阈值
         */
    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }
 
    /**
     * 通过以下两个获取next和prev的代码可以看出,entry数组实际上是一个环形结构
     */
    /**
      * Increment i modulo len.
      * 获取下一个索引,超出长度则返回0
      */
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }
 
    /**
     * Decrement i modulo len.
     * 返回上一个索引,如果-1为负数,返回长度-1的索引
     */
    private static int prevIndex(int i, int len) {
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }
 
    /**
     * 构造一个包含firstKey和firstValue的map。
     * ThreadLocalMap是惰性构造的,所以只有当至少要往里面放一个元素的时候才会构建它。
     */
    ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
        // 初始化table的大小为16
        table = new Entry[INITIAL_CAPACITY];
        // 通过hashcode & (长度-1)的位运算,确定键值对的位置
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        // 创建一个新节点保存在table当中
        table[i] = new Entry(firstKey, firstValue);
        // 设置table内元素为1
        size = 1;
        // 设置扩容阈值
        setThreshold(INITIAL_CAPACITY);
    }
 
    /**       
         * ThreadLocal本身是线程隔离的,按道理是不会出现数据共享和传递的行为的
         * 这是InheritableThreadLocal提供了了一种父子间数据共享的机制
         * @param parentMap the map associated with parent thread.
         */
    private ThreadLocalMap(ThreadLocalMap parentMap) {
        Entry[] parentTable = parentMap.table;
        int len = parentTable.length;
        setThreshold(len);
        table = new Entry[len];
 
        for (int j = 0; j < len; j++) {
            Entry e = parentTable[j];
            if (e != null) {
                @SuppressWarnings("unchecked")
                ThreadLocal key = (ThreadLocal) e.get();
                if (key != null) {
                    Object value = key.childValue(e.value);
                    Entry c = new Entry(key, value);
                    int h = key.threadLocalHashCode & (len - 1);
                    while (table[h] != null)
                        h = nextIndex(h, len);
                    table[h] = c;
                    size++;
                }
            }
        }
    }

ThreadLocalMap的内存泄漏问题

先来分析下弱引用

通过之前的博客强软弱虚引用,可知:弱引用在垃圾回收的时候是直接被回收的

为什么要使用弱引用呢?先来分析下如果使用强引用会是什么后果

ThreadLocal的原理分析以及ThreadLocalMap的源码分析_第5张图片

  1. 假设在业务代码中使用完ThreadLocal,即ThreafLocal引用被回收了;
  2. 但是因为ThreadLocalMap里Entry的key强引用了ThreadLocal对象,造成ThreadLocal无法被回收
  3. 在没有手动删除这个Entry以及CurrentThread依然运行的情况下,始终有强引用链:CurrentThread 引用->Thread 对象->ThreadLocalMap对象->Entry,这样Entry就不会被回收,这就导致了Entry的内存泄漏

总结:如果ThreaaLocalMap中Entry的key使用强引用,且不显式调用remove方法,是完全无法避免内存泄漏的

 

如果使用弱引用呢?

ThreadLocal的原理分析以及ThreadLocalMap的源码分析_第6张图片

 

  1. 同样假设在业务代码中使用完ThreadLocal,即ThreafLocal引用被回收了;
  2. 由于ThreadLocalMap里Entry的key只持有了ThreadLocal对象的虚引用,则没有任何强引用指向ThreadLocal对象,所以ThreadLocal对象可以被GC顺利回收,此时Entry中的key为null;
  3. 但是在没有手动删除这个Entry以及CurrentThread依然运行的情况下,也存在强引用链:CurrentThread 引用->Thread 对象->ThreadLocalMap对象->Entry->value,这样Entry里的value就不会被回收,这就导致了Entry的value内存泄漏

总结:如果ThreaaLocalMap中Entry的key使用弱引用,且不显式调用remove方法,也有可能发生内存泄漏

那value为啥不搞成弱引用,用完直接扔了多好?

不设置为弱引用,是因为不清楚这个Value除了map的引用还是否还存在其他引用,如果不存在其他引用,当GC的时候就会直接将这个Value干掉了,而此时我们的ThreadLocal还处于使用期间,就会造成Value为null的错误,所以将其设置为强引用。

而为了解决这个valkue强引用的问题,ThreadLocalMap提供了一种清除机制,我们下面会分析

 

ThreadLocal的原理分析以及ThreadLocalMap的源码分析_第7张图片

构造函数

/**
     * 构造一个包含firstKey和firstValue的map。
     * ThreadLocalMap是惰性构造的,所以只有当至少要往里面放一个元素的时候才会构建它。
     */
    ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
        // 初始化table的大小为16
        table = new Entry[INITIAL_CAPACITY];
        // 通过hashcode & (长度-1)的位运算,确定键值对的位置
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        // 创建一个新节点保存在table当中
        table[i] = new Entry(firstKey, firstValue);
        // 设置table内元素为1
        size = 1;
        // 设置扩容阈值
        setThreshold(INITIAL_CAPACITY);
    }

重点看一下上面构造函数中的int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);这一行代码。

ThreadLocal类中有一个被final修饰的类型为int的threadLocalHashCode,它在该ThreadLocal被构造的时候就会生成,相当于一个ThreadLocal的ID,而它的值来源于

private final int threadLocalHashCode = nextHashCode();
/*
 * 生成hash code间隙为这个魔数,可以让生成出来的值或者说ThreadLocal的ID较为均匀地分布在2的幂大小的数组中。
 */
private static final int HASH_INCREMENT = 0x61c88647;

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

 

可以看出,它是在上一个被构造出的ThreadLocal的ID(threadLocalHashCode)的基础上加上一个魔数0x61c88647的。

了解:这个魔数的选取与斐波那契散列有关,0x61c88647对应的十进制为1640531527。斐波那契散列的乘数可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769,如果把这个值给转为带符号的int,则会得到-1640531527。换句话说
(1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))得到的结果就是1640531527也就是0x61c88647。通过理论与实践,当我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀
ThreadLocalMap使用的是线性探测法,均匀分布的好处在于很快就能探测到下一个临近的可用slot,从而保证效率。这就回答了上文抛出的为什么大小要为2的幂的问题。为了优化效率。(摘自其他博客)

 

线性探测法(解决hash冲突的)

在开放定址算法里,线性探测法是散列解决冲突的一种方法,当hash一个关键字时,发现没有冲突,就保存关键字, 如果出现冲突,则就探测冲突地址下一个地址,依次按照线性查找,直到发现有空地址为止,从而解决冲突。

HashMap里面截图hash冲突使用的是链表法:链表法是一种更加常用的散列冲突解决办法,相比开放寻址法,它要简单很多。在散列表中,每个位置对应一条链表,所有散列值相同的元素都放到相同位置对应的链表中

对于& (INITIAL_CAPACITY - 1),相信有过算法经验或是阅读源码(比如HashMap)较多的程序员,一看就明白,对于2的幂作为模数取模,可以用&(2n-1)来替代%2n,位运算比取模效率高很多。至于为什么,因为对2^n取模,只要不是低n位对结果的贡献显然都是0,会影响结果的只能是低n位。之前的博文有分析,传送门:HashMap的容量为什么是2的n次幂小记

可以说在ThreadLocalMap中,形如key.threadLocalHashCode & (table.length - 1)(其中key为一个ThreadLocal实例)这样的代码片段实质上就是在求一个ThreadLocal实例的哈希值,只是在源码实现中没有将其抽为一个公用函数。

getEntry方法

private Entry getEntry(ThreadLocal key) {
             //根据key这个ThreadLocal的ID来获取索引,也即哈希值
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            //对应的entry存在且未失效且弱引用指向的ThreadLocal就是key,则返回
            if (e != null && e.get() == key)
                return e;
            else
                //如果第一次找的不对,因为用的是线性探测,所以往后找还是有可能能够找到目标Entry的
                return getEntryAfterMiss(key, i, e);
}

/*
 * 调用getEntry未直接命中的时候调用此方法
 */
private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
   
    // 基于线性探测法不断向后探测直到遇到空entry。
    while (e != null) {
        ThreadLocal k = e.get();
        // 找到目标
        if (k == key) {
            return e;
        }
        if (k == null) {
            // 该entry对应的ThreadLocal已经被回收,调用expungeStaleEntry来清理无效的entry
            expungeStaleEntry(i);
        } else {
            // 环形意义下往后面走
            i = nextIndex(i, len);
        }
        e = tab[i];
    }
    return null;
}

/**
 * 这个函数是ThreadLocal中核心清理函数,它做的事情很简单:
 * 就是从staleSlot开始遍历,将无效(弱引用指向对象被回收)清理,即对应entry中的value置为null,将指向这个entry的table[i]置为null,直到扫到空entry。
 * 另外,在过程中还会对非空的entry作rehash。
 * 可以说这个函数的作用就是从staleSlot开始清理连续段中的slot(断开强引用,rehash slot等)
 */
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // 因为entry对应的ThreadLocal已经被回收,value设为null,显式断开强引用
    tab[staleSlot].value = null;
    // 显式设置该entry为null,以便垃圾回收
    tab[staleSlot] = null;
    size--;

    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        ThreadLocal k = e.get();
        // 清理对应ThreadLocal已经被回收的entry
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            /*
             * 对于还没有被回收的情况,需要做一次rehash。
             * 
             * 如果对应的ThreadLocal的ID对len取模出来的索引h不为当前位置i,
             * 则从h向后线性探测到第一个空的slot,把当前的entry给挪过去。
             */
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;
                while (tab[h] != null) {
                    h = nextIndex(h, len);
                }
                tab[h] = e;
            }
        }
    }
    // 返回staleSlot之后第一个空的slot索引
    return i;
}

getEntry方法总结:
根据入参threadLocal的threadLocalHashCode对表容量取模得到下标index

  • 如果index对应的entry就是要读的threadLocal,则直接返回结果
  • 如果不是,调用getEntryAfterMiss线性探测,过程中每碰到无效entry,调用expungeStaleEntry进行段清理;如果找到了key,则返回结果entry
  • 没有找到key,返回null

看下流程图,方便记忆:

 set方法

private void set(ThreadLocal key, Object value) {

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len - 1);
    // 线性探测
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        ThreadLocal k = e.get();
        // 找到对应的entry,设置值
        if (k == key) {
            //如果新增的key与k相同则表示,当前数组存在相同key的Entry了,此时只需要更新value
            e.value = value;
            return;
        }
        //根据key计算的索引值,进行线性搜索后找到的第一个Key为空的Entry
        if (k == null) {
            //擦除key为空的Entry,并设置key和value,逻辑复杂
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    //这里我们来分析一下for循环的条件:
    //1.起始位置为 tab[i]:i = key.threadLocalHashCode & (len - 1);
    //2.循环的条件为:线性探测到的下一个 entry!= null
    //3.接着,继续使用nextIndex方法得到下一个位置(也就i=i+1的位置)如果在这个for循环里没有return,并且走完了(即线性探测到的下一个 entry == null),则继续执行下面的代码
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold) {
        rehash();
    }
}


/**
 * 启发式地清理slot,
 * i对应entry是非无效(指向的ThreadLocal没被回收,或者entry本身为空)
 * n是用于控制控制扫描次数的
 * 正常情况下如果log n次扫描没有发现无效slot,函数就结束了
 * 但是如果发现了无效的slot,将n置为table的长度len,做一次连续段的清理
 * 再从下一个空的slot开始继续扫描
 * 
 * 这个函数有两处地方会被调用,一处是插入的时候可能会被调用,另外个是在替换无效slot的时候可能会被调用,
 * 区别是前者传入的n为元素个数,后者为table的容量
 */
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        // i在任何情况下自己都不会是一个无效slot,所以从下一个开始判断
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            // 扩大扫描控制因子
            n = len;
            removed = true;
            // 清理一个连续段
            i = expungeStaleEntry(i);
        }
    } while ((n >>>= 1) != 0);
    return removed;
}



private void rehash() {
    // 做一次全量清理
    expungeStaleEntries();

    /*
     * 因为做了一次清理,所以size很可能会变小。
     * ThreadLocalMap这里的实现是调低阈值来判断是否需要扩容,
     * threshold默认为len*2/3,所以这里的threshold - threshold / 4相当于len/2
     */
    if (size >= threshold - threshold / 4) {
        resize();
    }
}

/*
 * 做一次全量清理
 */
private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        if (e != null && e.get() == null) {
            /*
             * 个人觉得这里可以取返回值,如果大于j的话取了用,这样也是可行的。
             * 因为expungeStaleEntry执行过程中是把连续段内所有无效slot都清理了一遍了。
             */
            expungeStaleEntry(j);
        }
    }
}

/**
 * 扩容,因为需要保证table的容量len为2的幂,所以扩容即扩大2倍
 */
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal k = e.get();
            if (k == null) {
                e.value = null; 
            } else {
                // 线性探测来存放Entry
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null) {
                    h = nextIndex(h, newLen);
                }
                newTab[h] = e;
                count++;
            }
        }
    }

    setThreshold(newLen);
    size = count;
    table = newTab;
}

 线性探测的过程中遇到key==null需要清除的entry时执行这个:

private void replaceStaleEntry(ThreadLocal key, Object value,int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;
        Entry e;
        //staleSlot是在set方法中通过key计算索引,经过线性探测,找到的第一个Key为null的Entry的所在位置,即:清除元素的开始位置
        //从staleSlot位置反向搜索,因为Entry数组的设计是环形的,因此反向遍历可以遍历到最后一个Entry为null的位置
        int slotToExpunge = staleSlot;
        for (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i,len))
            if (e.get() == null)
                // 用slotToExpunge记录最后一个key为null的索引位置
                slotToExpunge = i;
        //for循环执行完得到的是当前为空的entry的位置再往前探测到的第一个entry的key==null的位置

        //从staleSolt向后搜索,直到遇见空的Entry为止
        for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i,len)) {
            ThreadLocal k = e.get();
            //如果k与key相等,则将e的value设置为传入的value
            if (k == key) {
                e.value = value;
                // 将i位置和staleSlot位置的元素对换,如此以来开始遍历的位置就是i位置了,减少了需要遍历的元素,提高遍历效率(staleSlot位置是要清除的元素)
                tab[i] = tab[staleSlot];
                tab[staleSlot] = e;
                //如果slotToExpunge与staleSlot相等,则staleSlot位置表示的第一个key为null的Entry也是slotToExpunge表示的最后一个key为null的Entry,
                //即表示数组中只有一个key为null的Entry,上面已经将staleSlot位置的Entry放到了i位置,则此时清除的开始位置slotToExpunge应该为i
                if (slotToExpunge == staleSlot)
                    slotToExpunge = i;
                //从slotToExpunge位置开始清除key为空的Entry
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                return;
            }
            //如果key为空,而根据slotToExpunge与staleSlot相等可以知道数组中只有一个key为空的Entry,所以此时开始清除的位置slotToExpunge就是当前遍历到的位置i
            if (k == null && slotToExpunge == staleSlot)
                slotToExpunge = i;
        }
 
        // 上面的遍历没有遇见空的Entry,则将staleSlot位置的value设为空,并且在此位置放入新的Entry对象
        tab[staleSlot].value = null;
        tab[staleSlot] = new Entry(key, value);
 
        // 如果slotToExpunge不等于staleSlot表示,第一个key为空的Entry和最后一个key为空的Entry 不是同一个,也就是说Entry数组中存在多个Entry中key为空的对象,则从slotToExpunge位置开始清除
        if (slotToExpunge != staleSlot)
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    }

清理流程图: 

 set方法总结:

  • 探测过程中slot都有效,并且顺利找到key所在的slot,直接替换即可
  • 探测过程中发现有无效slot,调用replaceStaleEntry,效果是最终一定会把key和value放在这个slot,并且会尽可能清理无效slot
    • 在replaceStaleEntry过程中,如果找到了key,则做一个swap把它放到那个无效slot中,value置为新值
    • 在replaceStaleEntry过程中,没有找到key,直接在无效slot(一开始的地方)原地放entry
  • 探测没有发现key,则在连续段末尾的后一个空位置放上entry,这也是线性探测法的一部分。放完后,做一次启发式清理,如果没清理出去key,并且当前table大小已经超过阈值了,则做一次rehash,rehash函数会调用一次全量清理slot方法也即expungeStaleEntries,如果完了之后table大小超过了threshold - threshold / 4,则进行扩容2倍

 

你可能感兴趣的:(并发&JUC,java,数据结构,面试)