ThreadLocal 详解

ThreadLocal提供线程局部变量,确保每个线程都有自己独立的变量副本,避免数据共享。它与synchronized不同,不用于同步,而是用于数据隔离。ThreadLocal的set、get和remove方法涉及ThreadLocalMap,一个基于弱引用的内部类,使用开放地址法解决哈希冲突,避免内存泄漏。在使用ThreadLocal时,应主动调用remove方法,以防止内存泄漏。
摘要由CSDN通过智能技术生成

1、ThreadLocal简介

JDK源码对ThreadLocal类的注释如下:

  1. ThreadLocal提供线程局部变量,使得每个线程都有自己的、独立初始化的变量副本

  1. ThreadLocal实例通常是类中的private static字段,用于将状态与线程相关联,如用户ID、事务ID

  1. 只要线程处于活动状态并且ThreadLocal实例是可访问的,每个线程都将持有对线程局部变量副本的隐式引用

  1. 当线程终止,线程所绑定的线程局部变量都将被垃圾回收

下图可以增强理解:

2、ThreadLocal与Synchronized的区别

ThreadLocal和Synchonized都用于解决多线程并发访问,但是ThreadLocal与synchronized有本质的区别:

  1. Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。

  1. Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。

3、使用

方法名

描述

public void set( T value)

设置当前线程绑定的局部变量

public T get()

获取当前线程绑定的局部变量

public T remove()

移除当前线程绑定的局部变量,该方法可以帮助JVM进行GC

protected T initialValue()

返回当前线程局部变量的初始值


public class ThreadLocalDemo {
    private ThreadLocal<String> threadLocal = new ThreadLocal<>();
    private String name;
    public static void main(String[] args) {
        ThreadLocalDemo demo = new ThreadLocalDemo();
        for (int i = 0; i < 5; i++) {
            new Thread(() ->{
                demo.setName(Thread.currentThread().getName() + "的数据");
                System.out.println(Thread.currentThread().getName() + " : " + demo.getName());
            },"线程_" + i).start();
        }
    }

    public String getName() {
        return threadLocal.get();
    }

    public void setName(String name) {
        threadLocal.set(name);
    }
}

线程_0 : 线程_0的数据
线程_2 : 线程_2的数据
线程_3 : 线程_3的数据
线程_1 : 线程_1的数据
线程_4 : 线程_4的数据

4、ThreadLocal源码解析

4.1 set()


    public void set(T value) {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 判断map是否存在
        if (map != null)
            map.set(this, value);
        else
            // 初始化 thradLocalMap 并赋值
            createMap(t, value);
    }

    /**
     * 获取当前线程Thread对应维护的ThreadLocalMap 
     * 从这里可以看出,为什么说ThreadLocal 是线程本地变量来的了
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    /**
     * 创建当前线程Thread对应维护的ThreadLocalMap 
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

执行流程:

  1. 获取当前线程,并根据当前线程获取ThreadLocalMap

  1. 如果获取的ThreadLocalMap不为空,则将值 set 到ThreadLocalMap中(当前ThreadLocal的引用作为key)

  1. 如果ThreadLocalMap为空,则给该线程创建 ThreadLocalMap,并将第一个值存放进去

4.2 get()


    public T get() {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 如果此 map 存在
        if (map != null) {
            // 以当前的 ThreadLocal 为 key,调用 getEntry 获取对应的存储实体e
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 对e进行判空 
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 获取存储实体 e 对应的 value值
                T result = (T)e.value;
                return result;
            }
        }
        /*
            初始化 : 有两种情况执行下面代码
            第一种情况: map不存在,表示此线程没有维护的ThreadLocalMap对象
            第二种情况: map存在, 但是没有与当前ThreadLocal关联的entry
         */
        return setInitialValue();
    }

    /**
     * 初始化
     */
    private T setInitialValue() {
        // 调用initialValue获取初始化的值,此方法可以被子类重写, 如果不重写默认返回null
        T value = initialValue();
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 判断map是否存在
        if (map != null)
            // 存在则调用set方法设置值
            map.set(this, value);
        else
            // 初始化 thradLocalMap 并赋值
            createMap(t, value);
        // 返回设置的值value
        return value;
    }

    /**
     * 该方法是一个protected的方法,显然是为了让子类覆盖而设计的
     */
    protected T initialValue() {
        return null;
    }

执行流程:

  1. 获取当前线程, 根据当前线程获取ThreadLocalMap

  1. 如果获取的ThreadLocalMap不为空,则在ThreadLocalMap中以ThreadLocal的引用作为key来在ThreadLocalMap中获取对应的Entry e,如果e不为null,则返回e.value。

  1. 如果获取的ThreadLocalMap 不为空,但是 Entry 为空,则通过initialValue函数获取初始值value,调用set方法设置值

  1. 如果获取的ThreadLocalMap 为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的ThreadLocalMap

4.3 remove()


     public void remove() {
        // 获取当前线程对象中维护的ThreadLocalMap对象
         ThreadLocalMap m = getMap(Thread.currentThread());
        // 如果此map存在
         if (m != null)
            // 以当前ThreadLocal为key删除对应的实体entry
             m.remove(this);
     }

5、ThreadLocalMap

ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现的,而Entry又是ThreadLocalMap的内部类,且集成弱引用(WeakReference)类。


public class WeakRef {
    public static void main(String[] args) {
        WeakReference<String> sr = new WeakReference<String>(new String("hello"));
        System.out.println(sr.get()); // hello
        System.gc();
        System.out.println(sr.get()); // null
    }
}

    /**
     * Entry是一个弱引用
     * 如果只有Entry关联ThreadLocal对象,则gc时Entry关联的ThreadLocal对象会被回收
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
          
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

    // 初始容量,必须是2的整次幂
    private static final int INITIAL_CAPACITY = 16;

    // 存放数据的table
    private Entry[] table;

    // 数组里面存放entrys的个数,用于判断table当前使用量是否超过阈值。
    private int size = 0;

    // 进行扩容的阈值,使用量大于它的时候进行扩容。
    private int threshold;

5.1 弱引用

弱引用的出现就是为了垃圾回收服务的。它引用一个对象,但是并不阻止该对象被回收。如果使用一个强引用的话,只要该引用存在,那么被引用的对象是不能被回收的。弱引用则没有这个问题。在垃圾回收器运行的时候,如果一个对象的所有引用都是弱引用的话,该对象会被回收

Entry的Key为什么是弱引用?

如果key使用强引用:

  1. 业务代码中使用完ThreadLocal ,ThreadLocal Ref被回收了,

  1. 因为ThreadLocalMap的Entry强引用了threadLocal,造成threadLocal无法被回收,

  1. 在没有手动删除这个Entry以及CurrentThread依然运行的前提下,始终有强引用链 Thread ref->currentThread->threadLocalMap->entry,Entry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏

如果key使用弱引用:

  1. 业务代码中使用完ThreadLocal ,threadLocal Ref被回收了,

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

  1. 但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry->value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏。

value内存泄漏的补救措施

看源码你会发现在调用get、set或者remove()操作的时候,都有机会执行回收无效entry的操作。但是,这也不是一个十全十美的方法,考虑这样的场景:

  • 线程在后续的执行中,没有ThreadLocal对象执行get、set或remove方法

  • 线程的ThreadLocalMap中的过期entry将无法被清理,value的强引用链将一直存在,内存泄漏也将随之发生

不如主动调用ThreadLocal的remove方法,实现 Entry --> keyEntry --> valueThreaLocalMap --> Entry三大引用链的断开,避免内存泄漏的问题。

5.2 为什么ThreadLocalMap 采用开放地址法来解决哈希冲突?

JDK中大多数的类都是采用了链地址法来解决hash 冲突,为什么ThreadLocalMap 采用开放地址法来解决哈希冲突呢?首先我们来看看这两种不同的方式

链地址法

这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。

开放地址法

这种方法的基本思想是一旦发生了冲突,就去寻找下一个空的散列地址(这非常重要,源码都是根据这个特性,必须理解这里才能往下走),只要散列表足够大,空的散列地址总能找到,并将记录存入。

比如说,我们的关键字集合为{12,33,4,5,15,25},表长为10。 我们用散列函数f(key) = key mod l0。当计算前S个数{12,33,4,5}时,都是没有冲突的散列地址,直接存入(蓝色代表为空的,可以存放数据):

计算key = 15时,发现f(15) = 5,此时就与5所在的位置冲突。

于是我们应用上面的公式f(15) = (f(15)+1) mod 10 =6。于是将15存入下标为6的位置。这其实就是房子被人买了于是买下一间的作法:

链地址法和开放地址法的优缺点

开放地址法:

  1. 容易产生堆积问题,不适于大规模的数据存储。

  1. 散列函数的设计对冲突会有很大的影响,插入时可能会出现多次冲突的现象。

  1. 删除的元素是多个冲突元素中的一个,需要对后面的元素作处理,实现较复杂。

链地址法:

  1. 处理冲突简单,且无堆积现象,平均查找长度短。

  1. 链表中的结点是动态申请的,适合构造表不能确定长度的情况。

  1. 删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。

  1. 指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间。

ThreadLocalMap 采用开放地址法原因

  1. ThreadLocal 中看到一个属性 HASH_INCREMENT = 0x61c88647 ,0x61c88647 是一个神奇的数字,让哈希码能均匀的分布在2的N次方的数组里, 即 Entry[] table

  1. ThreadLocal 往往存放的数据量不会特别大(而且key 是弱引用又会被垃圾回收,及时让数据量更小),这个时候开放地址法简单的结构会显得更省空间,同时数组的查询效率也是非常高,加上第一点的保障,冲突概率也低

5.3 ThreadLoaclMap 构造器


    // 初始化ThreadLocalMap,并添加 firstValue到里面
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        //初始化table
        table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
        //计算索引
        // & (INITIAL_CAPACITY - 1) 相当于取模运算 hashCode % size 的一个更高效的实现
        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


    // ==> ThreadLocal类

    //AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减
    private static AtomicInteger nextHashCode =  new AtomicInteger();
    //特殊的hash值
    private static final int HASH_INCREMENT = 0x61c88647; 
    
    private final int threadLocalHashCode = nextHashCode();
    
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

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

5.4 set()


private void set(ThreadLocal<?> key, Object value) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        //计算索引
        int i = key.threadLocalHashCode & (len-1);

        // 从索引i开始,向后查找,直到遇到空slot
        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();

            if (k == key) {
                e.value = value;
                return;
            }

            // key为 null,说明之前的 ThreadLocal 对象已经被回收了,
            if (k == null) {
                //用新元素替换陈旧的元素,这个方法进行了不少的垃圾清理动作,防止内存泄漏
                replaceStaleEntry(key, value, i);
                return;
            }
        }
    
        //key 和 value 都为null,则在空元素的位置创建一个新的Entry。
        tab[i] = new Entry(key, value);
        int sz = ++size;
        // 清理过期的entry,如果size超过阈值则需要扩容
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
}

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

代码执行流程:

  1. 首先还是根据key计算出索引 i,然后查找i位置上的Entry

  1. 若是Entry已经存在并且key等于传入的key,那么这时候直接给这个Entry赋新的value值

  1. 若是Entry存在,但是key为null,则调用replaceStaleEntry来更换这个key为空的Entry

  1. 不断循环检测,直到遇到为null的地方,这时候要是还没在循环过程中return,那么就在这个null的位置新建一个Entry,并且插入,同时size增加1

  1. 最后调用cleanSomeSlots,清理key为null的Entry,最后返回是否清理了Entry,接下来再判断sz 是否>= thresgold达到了rehash的条件,达到的话就会调用rehash函数执行一次全表的扫描清理

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

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

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

  1. 可以把Entry[] table看成一个环形数组

5.4.1 replaceStaleEntry()

replaceStaleEntry() 方法并非简单地使用新entry替换过期entry,而是从过期entry所在的slot(staleSlot)向前、向后查找过期entry,并通过slotToExpunge 标记过期entry最早的index,最后使用cleanSomeSlots() 方法从slotToExpunge开始清理过期entry


    private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                   int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;
        Entry e;

        //表示开始探测式清理过期数据的开始下标,默认从当前的staleSlot开始
        int slotToExpunge = staleSlot;

        //从staleSlot的前一个位置开始,向前查找过期entry并更新slotToExpunge,直到遇到空slot
        for (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i, len))
            if (e.get() == null)
                slotToExpunge = i;
        
        // 从staleSlot的后一个位置开始,向后查找,直到遇到空slot
        for (int i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();
            // 由于开放定址法,可能相同的key存放于预期的位置(staleSlot)之后
            // 如果遇到相同的key,则更新value,并交换索引staleSlot与索引i的entry
            // 交换的原因:让有效的entry占据预期的位置(staleSlot),避免重复key的情况
            if (k == key) {
                e.value = value;
                tab[i] = tab[staleSlot];
                tab[staleSlot] = e;

                // slotToExpunge == staleSlot,说明索引staleSlot处前一个entry为null 
                // 未找到过期entry,更新slotToExpunge为i
                if (slotToExpunge == staleSlot)
                    slotToExpunge = i;
                // 从slotToExpunge开始,清理一些过期entry
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                return;
            }
            
            // 向后查找,未找到过期entry,更新slotToExpunge为当前index
            if (k == null && slotToExpunge == staleSlot)
                slotToExpunge = i;
        }

        // 直到遇到空slot也未发现相同的key,则在staleSlot的位置新建一个entry
        tab[staleSlot].value = null;
        tab[staleSlot] = new Entry(key, value);

        // 存在过期entry,需要进行清理
        if (slotToExpunge != staleSlot)
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    }

    /**
     * 获取环形数组的前一个索引
     */
    private static int prevIndex(int i, int len) {
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }

总体来说,过期entry所在的staleSlot是key瞄准的位置:

  • 如果key已经存在,则需要将原entry更新、与staleSlot对应的过期entry交换,使其位于staleSlot这个位置

  • 如果遇到了空slot,都未发现key相等的entry,说明key不存在 ⇒ \Rightarrow⇒ 直接在在staleSlot这个位置新建entry

  • 不管是哪种情况,只要发现过期entry,都需要通过cleanSomeSlots() 进行清理

  • 而过期entry存在的判断条件为:slotToExpunge != staleSlot

为何需要staleSlot和key相等时的slot交换?

通过向后遍历数组,找到了相同的key ,说明发生了hash冲突 的情况,让 key 存储到了预期位置的后面,staleSlot和key相等时的slot交换,让有效的entry占据预期的位置(staleSlot),在调用get方法获取时可直接通过hash & len-1得到准确的索引,而不用向后遍历去查找。

5.4.2 expungeStaleEntry()

清除当前过期entry到下一个空slot之间所有过期entry,并将有效entry通过hash & len-1重新计算索引位置,可能会遇到slot被占用的情况(开放地址法移位导致),需要向后遍历,找到空的slot放置,返回空slot的index


private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // 清理过期的entry
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // 对后续entry进行rehash,直到遇到空slot
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {  // 过期entry,继续清理
            e.value = null;
            tab[i] = null;
            size--;
        } else { // 有效entry,rehash到合适的位置(补齐空slot)
            int h = k.threadLocalHashCode & (len - 1);
            // 期望的index与当前index不相等,说明是开放地址法移位导致的,需要将其放到最近的有效entry之后
            if (h != i) { 
                tab[i] = null;
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i; // 返回空slot的index
}

5.4.3 cleanSomeSlots()

通过循环扫描,尽可能多的清理ThreadLocalMap中的过期entry


private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) { // 遇到过期entry,需要重置n
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0); //无符号右移动一位,可以简单理解为除以2
    return removed;
}

5.4.4 rehash()

rehash之前仍然先清理一次过期entry,如果size > = 3/4 threshold,也就是size >= 1/2 table.length则进行扩容操作( threshold = 2/3 * table.length)


private void rehash() {
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        resize();
}

5.4.5 expungeStaleEntries()

对数组进行整体的遍历,清理过期的key,cleanSomeSlots()是尽可能多的清理,不一定清理的干净


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)
            // 清除当前过期entry到下一个空slot之间所有过期entry,并将有效entry进行 hash & len-1,
            // 重新计算其在数组中的位置
            expungeStaleEntry(j);
    }
}

5.4.6 resize

将原数组扩大为原来的两倍,将旧的桶数组中的entry移动到新的桶数组中,重新计算其索引位置,遇到过期entry,直接断开entry对value的引用,方便gc。


private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;
    // 旧的桶数组中的entry移动到新的桶数组中
    // 对于过期entry,直接断开entry对value的引用
    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }
    // 更新threshold、size、table,旧的桶数组等待GC
    setThreshold(newLen);
    size = count;
    table = newTab;
}

5.5 getEntry()

获取key对应的entry,若直接命中,则直接返回对应的entry;否则,需要通过getEntryAfterMiss() 方法往后遍历查找


private Entry getEntry(ThreadLocal<?> key) {
    // 计算索引位置
    int i = key.threadLocalHashCode & (table.length - 1); 
    Entry e = table[i];
    if (e != null && e.get() == key)  // 直接命中
        return e;
    else  
        // 否则,往后遍历查找,说明出现hash冲突问题
        return getEntryAfterMiss(key, i, e);
}

5.5.1 getEntryAfterMiss()


private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    // 从 e 的下一个索引处向后遍历,遇到空slot结束
    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        // 遇到过期的entry
        // 清除过期entry到下一个空slot之间所有过期entry,并将有效entry通过hash & len-1重新计算索引位置
        if (k == null) 
            expungeStaleEntry(i); 
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

5.6 remove()


private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    // 计算索引位置
    int i = key.threadLocalHashCode & (len-1);
    // 向后遍历,直到遇到空slot
    for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
        // 找到key
        if (e.get() == key) {
            // 清除e
            e.clear();
          //清除过期entry到下一个空slot之间所有过期entry,并将有效entry通过hash & len-1重新计算索引位置
            expungeStaleEntry(i);
            return;
        }
    }
}

6、ThreadLocal 总结

6.1 TreadLocal变量通常设置为static的原因

为了避免重复创建TSO(thread specific object,即与线程相关的变量),我们知道,一个ThreadLocal实例对应当前线程中的一个TSO实例。因此,如果把ThreadLocal声明为某个类的实例变量(而不是静态变量),那么每创建一个该类的实例就会导致一个新的TSO实例被创建。显然,这些被创建的TSO实例是同一个类的实例。于是,同一个线程可能会访问到同一个TSO(指类)的不同实例,这即便不会导致错误,也会导致浪费(重复创建等同的对象)!因此,一般我们将ThreadLocal使用static修饰即可。

6.2 常见使用场景

  1. 每个线程需要有自己单独的实例

  1. 实例需要在多个方法中共享,但不希望被多线程共享

1)保存线程不安全的工具类

ThreadLocal解决SimpleDateFormat线程安全问题

SimpleDateFormat(下面简称sdf)类内部有一个Calendar对象引用,它用来储存和这个sdf相关的日期信息,例如sdf.parse(dateStr), sdf.format(date)诸如此类的方法参数传入的日期相关String,Date等等,都是交给Calendar引用来储存的。这样就会导致一个问题,如果你的sdf是个static的,那么多个thread 之间就会共享这个sdf,同时也是共享这个Calendar引用,并且,观察sdf.parse()方法,你会发现有如下的调用:


Date parse() {
  calendar.clear(); // 清理calendar
  ... // 执行一些操作, 设置 calendar 的日期什么的
  calendar.getTime(); // 获取calendar的时间
}

这里会导致的问题就是,如果线程A调用了sdf.parse(),并且进行了calendar.clear()后还未执行calendar.getTime()的时候,线程B又调用了sdf.parse(),这时候线程B也执行了sdf.clear()方法,这样就导致线程 A 的的calendar数据被清空了(实际上A,B的同时被清空了)。又或者当 A 执行了calendar.clear()后被挂起,这时候 B 开始调用sdf.parse()并顺利结束,这样 A 的 calendar内存储的的date 变成了后来 B 设置的calendar的date。

解决方案

最简单的解决方案我们可以把static去掉,这样每个新的线程都会有一个自己的sdf实例,从而避免线程安全的问题。然而,使用这种方法,在高并发的情况下会大量的new sdf以及销毁sdf,这样是非常耗费资源的,所以是不可行的。

另外一种方法可以使用Threadlocal解决此问题,对于每个线程SimpleDateFormat不存在影响他们之间协作的状态,为每个线程创建一个SimpleDateFormat变量的拷贝或者叫做副本,代码如下:


/** 
 * 使用ThreadLocal以空间换时间解决SimpleDateFormat线程安全问题。 
 */  
public class DateUtil {  

    private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";  

    @SuppressWarnings("rawtypes")  
    private static ThreadLocal threadLocal = new ThreadLocal() {  
        protected synchronized Object initialValue() {  
            return new SimpleDateFormat(DATE_FORMAT);  
        }  
    };  

    public static DateFormat getDateFormat() {  
        return (DateFormat) threadLocal.get();  
    }  

    public static Date parse(String textDate) throws ParseException {  
        return getDateFormat().parse(textDate);  
    }  
}

创建一个ThreadLocal类变量,这里创建时用了一个匿名类,覆盖了initialValue方法,主要作用是创建时初始化实例,也可以采用下面方式创建。


//第一次调用get将返回null  
private static ThreadLocal threadLocal = new ThreadLocal();  
//获取线程的变量副本,如果不覆盖initialValue,第一次get返回null,故需要初始化一个SimpleDateFormat,并set到threadLocal中  
public static DateFormat getDateFormat() {  
    DateFormat df = (DateFormat) threadLocal.get();  
    if(df==null){
        df = new SimpleDateFormat(DATE_FORMAT)  
        threadLocal.set(df);  
    }  
    return df;  
}

2)数据跨层传递

每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。

例如,用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。

在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(如 user 对象)作为参数传递的麻烦。

比如说我们是一个用户系统,那么当一个请求进来的时候,一个线程会负责执行这个请求,然后这个请求就会依次调用service-1()、service-2()、service-3()、service-4(),这4个方法可能是分布在不同的类中的。这个例子和存储session有些像。


public class ThreadLocalDemo {
    public static void main(String[] args) {
        User user = new User("jack");
        new Service1().service1(user);
    }
 
}
 
class Service1 {
    public void service1(User user){
        //给ThreadLocal赋值,后续的服务直接通过ThreadLocal获取就行了。
        UserContextHolder.holder.set(user);
        new Service2().service2();
    }
}
 
class Service2 {
    public void service2(){
        User user = UserContextHolder.holder.get();
        System.out.println("service2拿到的用户:"+user.name);
        new Service3().service3();
    }
}
 
class Service3 {
    public void service3(){
        User user = UserContextHolder.holder.get();
        System.out.println("service3拿到的用户:"+user.name);
        //在整个流程执行完毕后,一定要执行remove
        UserContextHolder.holder.remove();
    }
}
 
class UserContextHolder {
    //创建ThreadLocal保存User对象
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}
 
class User {
    String name;
    public User(String name){
        this.name = name;
    }
}
 
执行的结果:
 
service2拿到的用户:jack
service3拿到的用户:jack

参考文章:

被大厂面试官连环炮轰炸的ThreadLocal (吃透源码的每一个细节和设计原理) - 掘金 (juejin.cn)

(1条消息) ThreadLocal学习_晓之木初的博客-CSDN博客

(1条消息) 史上最全ThreadLocal 详解(一)_倔强的不服的博客-CSDN博客_threadlocal

(1条消息) [JAVA]深入讲解ThreadLocal变量通常设置为static的原因_不服老的码农的博客-CSDN博客

(1条消息) ThreadLocal解决SimpleDateFormat线程安全问题_MrAlgernon的博客-CSDN博客

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值