深入理解 Java - ThreadLocal

深入理解ThreadLocal

一、ThreadLocal简介

1.1 概念

ThreadLocal 听名知意,即是 线程变量,主要作用是用来存储线程本地变量,适用于隔离线程间变量,而在方法或类间共享的场景

它可以理解成是一种以自身实例为键, 任意类型对象为值的键值对存储结构。这种数据结构可以被附带到每一个线程中,也就是说,一个线程都可以根据一个ThreadLocal实例查询到绑定在自己身上的一个值;

每个Thread都有自己的ThreadLocal且只能由当前Thread使用ThreadLocal是每个线程独有的,在多个线程操作共享变量时,会出现线程安全问题,但如果一个变量是线程独有的,对于其他线程不可见的,各自操作各自的,自然不涉及线程安全问题

可以通过声明一个任意泛型的ThreadLocal对象,并在不同线程中使用它,就会在不同线程中创建多个实例副本,即同一个ThreadLocal对象在不同线程中使用可以存储不同的值;


1.2 结构图

ThreadLocal在每个线程内涉及的调用,包括方法、类中都是共享的,且不需要进行传参,可以理解为一个线程内部的全局变量,注意:只在一个线程中有效。

下图可以增强理解:

在这里插入图片描述


1.3 核心函数

方法声明描述
protected T initialValue()返回当前线程局部变量的初始值
public void set(T value)设置当前线程绑定的局部变量
public T get()获取当前线程绑定的局部变量
public void remove()移除当前线程绑定的局部变量

二、ThreadLocal简单使用

2.1 测试代码

下面的例子中,分别声明了两个ThreadLocal对象实例A、B,并初始化值。

在主线程中调用了ThreadLocal的相关操作,同时穿插调用线程池执行任务,并在任务中调用ThreadLocal,猜猜看执行结果是啥?

/**
 * ThreadLocal相关测试
 *
 * @author Jiechong Bai
 * @since 2023-09-06 22:08
 */
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ThreadLocalTest.class)
public class ThreadLocalTest {

    /**
     * 设置ThreadLocal初始化值
     */
    private static final ThreadLocal<String> A = ThreadLocal.withInitial(() -> "A");

    private static final ThreadLocal<String> B = ThreadLocal.withInitial(() -> "B");
    
    @Test
    public void run1() {
        A.set("A-MAIN");
        log.info("Main thread value is : {}", A.get()); 
        
        ThreadPoolUtils.execute(() -> {
            A.set("A-THREAD");
            log.info("Child t1 {} thread value is : {}", Thread.currentThread().getName(), A.get()); 
            log.info("Child t1 {} thread value is : {}", Thread.currentThread().getName(), B.get()); 
            A.remove();
            B.remove();
        });
        
        B.set("B-MAIN");
        log.info("Main thread value is : {}", B.get()); 
        
        ThreadPoolUtils.execute(() -> {
            B.set("B-THREAD");
            log.info("Child t2 {} thread value is : {}", Thread.currentThread().getName(), A.get());
            log.info("Child t2 {} thread value is : {}", Thread.currentThread().getName(), B.get());
            A.remove();
            B.remove();
        });
        
        A.remove();
        B.remove();
    }
}

下面是运行后的结果:

//可以看到,主线程分别打印A-MAIN、B-MAIN;
Main thread value is : A-MAIN
Main thread value is : B-MAIN

//两次执行任务是两个新的线程Child t1、Child t2   
//Child t1 打印 A-THREAD(手动set)、B(初始值);    
Child t1 pool-1-thread-1 thread value is : A-THREAD
Child t1 pool-1-thread-1 thread value is : B

//Child t2 打印 A(初始值)、B-THREAD(手动set);    
Child t2 pool-1-thread-2 thread value is : A
Child t2 pool-1-thread-2 thread value is : B-THREAD

2.2 分析执行流程

简单分析下上面例子的执行流程:

1、类加载时,会先加载静态常量ThreadLocal AThreadLocal B的初始值分别为"A"、“B”。

2、进入主线程,此时,可以理解为在主线程绑定的ThreadLocal中,ThreadLocal A、B的初始值分别是“A”、“B”,调用 A.set("A-MAIN"),此时ThreadLocal A 的值由初始值 “A” 改变为 “A-MAIN”,此时ThreadLocal A、B 在 主线程中的值分别为 “A-MAIN”、“B”;

3、由于使用线程池异步执行,所以这里主线程大概率先执行完,依次打印出A-MAIN、B-MAIN,并执行remove()

4、进入子线程 t1中,此时,在子线程 t1 绑定的ThreadLocal中, ThreadLocal A、B的初始值分别是 “A”、“B”,调用 A.set("A-THREAD"),此时ThreadLocal A 的值由初始值 “A” 改变为 “A-THREAD”,此时ThreadLocal A、B 在子线程 t1 中的值分别为 “A-THREAD”、“B”,打印结果后 执行remove();

5、进入子线程 t2中,此时,在子线程 t2 绑定的ThreadLocal中, ThreadLocal A、B的初始值分别是 “A”、“B”,调用 B.set("B-THREAD"),此时ThreadLocal B 的值由初始值 “B” 改变为 “B-THREAD”,此时ThreadLocal A、B 在子线程 t2 中的值分别为 “A”、“B-THREAD”,打印结果后 执行remove();

根据流程并结合 1.3 的结构图来看,这里已经非常明显的看到ThreadLocal的特性了,各个线程之间操作互不干扰,ThreadLocal的值是互相独立的。


三、ThreadLocal实现原理

3.1 源码分析

下面我会把我认为比较核心的点单拎出来,并结合源码一行行分析

3.1.1 ThreadLocal.Class 结构

在这里插入图片描述

ThreadLocal类中有两个静态内部类SuppliedThreadLocalThreadLocalMap, 其中ThreadLocalMap就是ThreadLocal机制的核心之一,非常重要,后面会说到。

除了两个静态内部类以外,还有一系列对外提供的方法,下面会列出核心函数来分析。

另外还有三个私有成员变量,都是ThreadLocalMap用来存储内容时用到的,后面都会一一解读


3.1.2 ThreadLocalMap.Class(静态内部类)
static class ThreadLocalMap {

   //(1) ThreadLocalMap的静态内部类
   //(2) 键值对结构类Entry,继承了弱引用,意将为key的ThreadLocal对象设为弱引用
   static class Entry extends WeakReference<ThreadLocal<?>> {
        
        //(3) 键值对中的Value,在下面构造方法中被赋值
        Object value;
       
        //(4) 此构造供外部调用,用于创建一个全新的Entry节点
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

    //(5) Entry[] table 数组的初始容量为16     
    private static final int INITIAL_CAPACITY = 16;

    //(6) 用来存储 Entry类型数组,可以看做是kv键值对的数组
    private Entry[] table;

    //(7) table数组长度
    private int size = 0;

    //(8) 可以理解为触发自动扩容的阈值,默认为0
    private int threshold; 
    
    //(9) resize扩容阈值加载因子为2/3,也就是三分之二
    private void setThreshold(int len) {
         threshold = len * 2 / 3;
    }
    
    //...此处省略ThreadLocalMap.Class的方法
}    

ThreadLocalMapThreadLocal的一个静态内部类。每一个Thread对象实例中都维护了一个ThreadLocalMap对象(下面 3.1.3 会说到),它本质上是一组以ThreadLocal为key,任意类型值为value的K-V键值对。

另外,ThreadLocalMap内部还维护了一个Entry静态内部类,该类继承了WeakReference(弱引用,该类的引用在虚拟机进行GC时会被进行清理),并指定所引用的泛型为ThreadLocal类型,Entry是一个键值对结构,而真正的内容都在table中存储。

//一组以ThreadLocal为key,任意类型值为value的K-V键值对
private Entry[] table;

看到这里,是不是觉得ThreadLocalMap有点HashMap的味道了,整个ThreadLocal中核心内容都是围绕ThreadLocalMap进行操作,而ThreadLocalMap的核心内容都是在围绕 Entry[] table 的数组键值对存储结构进行操作。

关于Thread、ThreadLocal、ThreadLocalMap之间的关系,下图可以增强理解:

在这里插入图片描述


3.1.3 threadLocalHashCode 与 HASH_INCREMENT

首先说结论HASH_INCREMENTthreadLocalHashCode是通过一个固定的Hash散列算法来决定ThreadLocal在数组中的实际存储下标,即是决定ThreadLocal在作为key存入Entry[] table的哪个下标,取值时同样也要用相同的算法求出下标,关于散列的作用这里不再过多阐述了。

HASH_INCREMENT,翻译过来就是 增量哈希, 它是threadLocalHashCode用来计算下一个哈希码时需要用到的魔数,源码中用十六进制表示,用转换器转换下就可以得出对应的十进制是 1640531527

private final int threadLocalHashCode = nextHashCode();

private static AtomicInteger nextHashCode = new AtomicInteger();

private static final int HASH_INCREMENT = 0x61c88647;

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

官方对HASH_INCREMENT的注释:

连续生成的哈希代码之间的差异 - 将隐式顺序线程本地 ID 转换为两大小表的近乎最佳分布的乘法哈希值。

我刚开始其实并不理解这个数字,直到在网上找到一些解答,最后才知道1640531527是在有符号的int范围内是黄金分割数;

在ThreadLocalMap的源码中的getEntry()方法中可以看到,获取值时求下标的算法为:

keyIndex = key.threadLocalHashCode & (table.length - 1);

这个算法就可以简单看做是对当前table长度的取模,我看到这块的时候很眼熟,因为与HashMap中对散列的处理大差不差,这里就是通过threadLocalHashCode取模出一个位于当前长度中一个合适的下标, 之所以用位运算是因为&的效率要比%高得多,主要原因是位运算是直接操作内存的,不需要再转成十进制数据,因此处理速度快

不止getEntry()中,其他有使用到threadLocalHashCode的地方,不管是存入还是取值都是此算法,下面列出几处ThreadLocalMap的源码:

private Entry getEntry(ThreadLocal<?> key) {
    //计算出下标,table.length 即 Entry[] table当前长度
    int i = key.threadLocalHashCode & (table.length - 1);//散列算法
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

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 {
                //计算出下标,table.length 即 Entry[] table当前长度
                int h = k.threadLocalHashCode & (newLen - 1); //散列算法
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }
    setThreshold(newLen);
    size = count;
    table = newTab;
}

照着源码中的方式,比葫芦画瓢,写一个在固定长度中求散列值的demo:

public class TestThreadLocalHashCode {
 
    public static void main(String[] args) {
        hashCode(4);
        hashCode(16);
        hashCode(32);
        hashCode(64);
    }
    
    private static final int HASH_INCREMENT = 0x61c88647;
 
    //每次调用相当于在一个ThreadLocalMap在固定长度下生成所有的下标
    private static void hashCode(int capacity) {
        final AtomicInteger nextHashCode = new AtomicInteger(HASH_INCREMENT);
        for (int i = 0; i < capacity; i++) {
            int keyIndex = nextHashCode.getAndAdd(HASH_INCREMENT) & (capacity - 1);
            System.out.print(keyIndex + " ");
        }
        System.out.println();
    }
}

在不触发二次扩容的场景下,每个ThreadLocalMap中的长度分别为4,16,32,64,输出结果:

3 2 1 0 //4
    
7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0 //16
    
7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0  //32
    
7 14 21 28 35 42 49 56 63 6 13 20 27 34 41 48 55 62 5 12 19 26 33 40 47 54 61 4 11 18 25 32 39 46 53 60 3 10 17 24 31 38 45 52 59 2 9 16 23 30 37 44 51 58 1 8 15 22 29 36 43 50 57 0 //64

从打印的结果可以看出来,在散列后都刚好填满了整个数组长度,尽可能避免hash冲突,实现了完美散列,这样在使用中就尽可能的提高了在ThreadLocalMap中获取元素的命中率,尽可能提高效率。

3.1.4 与Thread.Class 的关联

点击内部类ThreadLocalMap发现:

在这里插入图片描述

进入Thread类中,可以看到类中声明了一个ThreadLocal.ThreadLocalMap threadLocals 的属性,除此之外还有一个相同类型的 ThreadLocal.ThreadLocalMap inheritableThreadLocals,它们两个都是ThreadLocal.ThreadLocalMap类型, 通过查看ThreadLocalMap类得知,实际上它类似于一个HashMap,在默认情况下,这两个变量的初始值都为null,如下:

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

这里可以理解为,在一个线程中,通过声明出一个类型ThreadLocalMap类型的键值对结构,从而实现 在一个线程上 绑定多个键值对内容,通过上一步查看ThreadLocalMap类得知,keyThreadLocal对象本身,value为任意类型。

可能和一开始想的不一样,每个线程的本地变量不是存储在ThreadLocal实例中,而是存储在调用线程实例中的threadLocals变量中,也就是说,代码中ThreadLocal类型的本地变量是存放在具体的线程上,那ThreadLocal本身是不是就可以理解为一个类似工具类的存在?

到这里自然也理解了为何线程之间的操作相互隔离,互不影响,且不存在线程安全问题,本质上都是每个线程在基于不同ThreadLocal作为key,操作自己私有的threadLocals 变量


3.1.5 初始化函数

ThreadLocal提供两种初始化默认值的方式,不论哪种方式,都是通过子类化并重写initialValue()方法来实现的。

  • 方式一 :声明匿名内部类重写ThreadLocalinitialValue()函数

在这里插入图片描述

//定义ThreadLocal时,使用匿名内部类的洗的写法
private static final ThreadLocal<String> t = new ThreadLocal<String>(){
     @Override
     protected String initialValue() {
         //此处可以自定义默认值
         return super.initialValue();
     }
};
  • 方式二 :静态内部类SuppliedThreadLocal继承自ThreadLocal,并重写initialValue()方法,加上函数式接口,lambda表达式初始化默认值;
private static final ThreadLocal<String> A = ThreadLocal.withInitial(() -> "A");
//Supplier为函数式接口,调用静态内部类SuppliedThreadLocal的构造方法初始化默认值
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}
//继承ThreadLocal,并重写initialValue()方法
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
    
    //函数式接口定义
    private final Supplier<? extends T> supplier;
    
    //构造函数
    SuppliedThreadLocal(Supplier<? extends T> supplier) {
        this.supplier = Objects.requireNonNull(supplier);
    }
    
    //实际调用
    @Override
    protected T initialValue() {
        return supplier.get();
    }
}

3.1.6 set()方法源码
public void set(T value) {
    //(1)、获取当前线程(调用者线程)
    Thread t = Thread.currentThread();
    //(2)、以当前线程 t 作为getMap()参数,获取线程实例内的threadLocals变量
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //(3)、如果map不为null,则调用ThreadLocalMap的set()方法添加进Entry[] 数组
        map.set(this, value);
    else
        //(4)、如果map为null,则说明线程内threadLocals变量为默认值null, 本次是首次添加;
        //则将调用者线程、本地变量value作为参数,调用createMap创建map
        createMap(t, value);
}

1处代码Thread.currentThread()即获取当前调用此方法的线程(调用者线程)

2处代码getMap()源码如下,可以看到直接返回调用者线程 t 的threadLocals变量。

ThreadLocalMap getMap(Thread t) {
    //直接返回当前调用者线程 t 的threadLocals 变量
    return t.threadLocals;
}

3处代码:如果getMap()返回不为null,则直接调用ThreadLocalMap.set()this、本地变量value设置到threadLocals中,这里的this是精髓,已知this即是当前ThreadLocal实例,是map的key,而value则是对应的本地变量

4处代码createMap()源码如下:

void createMap(Thread t, T firstValue) {
    //调用内部类ThreadLocalMap的构造方法进行初始化
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

当一个线程第一次调用createMap()方法前,t.threadLocals == null,这里直接new了一个ThreadLocalMap实例赋值给t.threadLocals,调用的构造方法如下:

//见名知意,firstKey: 第一个key, firstValue:第一个value
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    //初始化Entry[] 类型数组,并赋予初始化长度 INITIAL_CAPACITY
    table = new Entry[INITIAL_CAPACITY];
    //根据当前ThreadLocal实例hash值与初始化容量做与运算,得出当前存储下标
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    //真正存入Entry[] table的某个下标中
    table[i] = new Entry(firstKey, firstValue);
    //初始化长度为1
    size = 1;
    //这里应该是计算下一次自动扩容的长度
    setThreshold(INITIAL_CAPACITY);
}

构造方法中初始化了ThreadLocalMap相关信息,不仅创建了threadLocals,同时也将 firstKey, firstValue添加到了threadLocals中;

所以这里可以得出,当一个线程第一次调用set(Object obj)方法时,会创建此线程中的 threadLocals变量,并将所引用的ThreadLocal 实例作为 firstKey,本次set的值作为firstValue存入到此threadLocals变量的Entry[] entry 数组键值对中去;

注意: 如果在线程中继续重复调用set()方法,不会再走createMap()方法, 因为此线程的threadLocals变量在第一次调用时已经初始化过了,getMap(t) 返回不是null,后续就是基于不同的ThreadLocal实例作为key 存入 Entry[] entry 中。

当然,如果在一个线程中重复调用同一个ThreadLocal实例的set()方法,则是后者值覆盖前者值的情况,与HashMap一样;

下面是同一个ThreadLocal实例值覆盖的代码示例以及debug截图:

private static final ThreadLocal<String> A = ThreadLocal.withInitial(() -> "A");

public void run1() {
    ThreadPoolUtils.execute(() -> {
        //重复设置 ThreadLocal A
        A.set("A-THREAD");
        A.set("A-THREAD-01");
        log.info("Child t1 {} thread value is : {}", Thread.currentThread().getName(), A.get());
        log.info("Child t1 {} thread value is : {}", Thread.currentThread().getName(), B.get()); 
        A.remove();
        B.remove();
    });
}

在这里插入图片描述


3.1.7 get()方法源码
public T get() {
    //(1)、获取当前线程(调用者线程)
    Thread t = Thread.currentThread();
    //(2)、以当前线程 t 作为getMap()参数,获取线程实例内的threadLocals变量
    ThreadLocalMap map = getMap(t);
    //(3)、如果调用者线程内部的threadLocals变量不为null
    if (map != null) {
        //(4)、调用getEntry(this)获取此key(ThreadlLocal实例)对应的value
        ThreadLocalMap.Entry e = map.getEntry(this);
        //(5)、如果获取到的Entry对象不为null,则返回e.value
        if (e != null) {
            @SuppressWarnings("unchecked") //类型强转去除警告
            T result = (T)e.value;
            return result;
        }
    }
    //(6)、一旦3,5两步代码某一个为null,则会调用此方法创建map或者set值
    return setInitialValue();
}
  • 1、2、3处代码与set()方法一致,上面已经详细说过,这里不再阐述;

其中有趣的点在于3、5处代码的两个if判空:

  • 3处代码如果为false,说明此线程所绑定的threadLocals == null,还没初始化过;

  • 5处代码如果为false,则说明threadLocals已经初始化过,只是内部的 Entry[] table 还没有存入key为当前ThreadLocal实例的内容;

二者不管谁为false,都会走到第6处代码,调用下面的setInitialValue()方法。

private T setInitialValue() {
    //(7)、调用initialValue()方法获取默认值
    T value = initialValue();
    //(8)、获取当前线程(调用者线程)
    Thread t = Thread.currentThread();
    //(9)、以当前线程 t 作为getMap()参数,获取线程实例内的threadLocals变量
    ThreadLocalMap map = getMap(t);
    //(10)、如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
    if (map != null)
        map.set(this, value);
    else
        //(11)、如果map为null,说明首次添加,需要首先创建出对应的map
        createMap(t, value);
    //(12)、返回value,即是初始化的默认值
    return value;
}
  • 7处代码:调用initialValue()方法获取默认值,此方法对应不同的初始化方式调用的方式也不一样,涉及到 3.1.4 中两种初始化函数中重写的initialValue()方法,如果是使用这两种初始化方式,根据继承特性,则调用时分别对应到以下位置:

方式一:声明匿名内部类重写ThreadLocalinitialValue()函数

private static final ThreadLocal<String> t = new ThreadLocal<String>(){
    //调用此处
    @Override
    protected String initialValue() {
        return "initValue";
    }
};

方式二:静态内部类SuppliedThreadLocal继承自ThreadLocal,并重写initialValue()方法

private static final ThreadLocal<String> A = ThreadLocal.withInitial(() -> "initValue");

static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
    
    private final Supplier<? extends T> supplier;
    
    SuppliedThreadLocal(Supplier<? extends T> supplier) {
        this.supplier = Objects.requireNonNull(supplier);
    }
    
    //调用此处
    @Override
    protected T initialValue() {
        return supplier.get();
    }
}

方式三:只调用new ThreadLocal(),则无重写,直接调用ThreadLocal类中的initialValue()方法,返回null。

private static final ThreadLocal<String> t1 = new ThreadLocal<>();

//调用此处
protected T initialValue() {
    return null;
}
  • 8、9处代码:与get()一致,不再阐述;

  • 10、11处代码:对应上了get()方法中的5、3两处的if判空,对应两种情况分别处理;如果map != null 对应5处代码,说明table 还没有存入key为当前ThreadLocal实例的内容,则直接调用ThreadLocalMap.set()赋值到table数组中;如果map == null 则对应 3处代码,说明此线程所绑定的threadLocals == null,还没初始化过,则调用createMap()创建map,并将firstKey、firstValue赋值到table数组中;

  • 12处代码:最终返回value给调用者。

3.1.8 remove() 方法源码

此方法在 4.2 内存泄漏问题 中做解读


3.2 总结

通过阅读源码,我大致了解到ThreadLocal本质只是一个工具类,它将自身Hash值作为key,并提供了initialValue()、set()、get()、remove()等方法操作实际存放在线程Thread实例内的 ThreadLocal.ThreadLocalMap threadLocals变量;

同时通过对threadLocalHashCode的完美散列,尽可能的提高使用效率,其实关于散列这块的处理和HashMap的hash方法有异曲同工之处,我在写的同时又去复习了一遍HashMap的相关原理。

threadLocals是基于ThreadLocal的内部类ThreadLocalMapThreadLocalMap类中又提供了一个名为Entry的内部类,算是套娃了。

除此之外,ThreadLocalMap内部还提供了Entry类型的table数组用于存储键值对,以及一系列操作table数组的方法,这些方法才是真正操作threadLocals的角色。

可以理解为在ThreadLocalMap外面封装了一层ThreadLocal工具类,并且将ThreadLocal作为key,表面上调用的都是ThreadLocal提供的方法,实际操作threadLocals都出自ThreadLocalMap之手。

其中的精髓在于用类与类之间的组合使用,同时还用了一种十分巧妙的方式,常见的Thread.currentThread()获取当前调用者线程 、以及调用getMap(this)、createMap(this, value)等对this的巧妙使用,十分值得借鉴与学习。


四、使用ThreadLocal存在的问题

4.1 线程池问题

到目前为止,我个人所发现的问题,如果在线程池任务中使用ThreadLocal需要注意的点,这里可以直接看我2.1测试代码的demo,其中就是使用线程池提交任务,并在任务中调用了ThreadLocal

由于线程池中是用线程复用的方式,在线程池中的线程,尤其是核心线程数,只要项目不挂,甚至永远不会结束,使用不当就会造成一些问题;重复调用例子如果不调用remove()则一定会取到之前同一个线程的旧值(可以把线程池核心线程数设置为1,更容易看到效果)。

例如:Thread t1 在第1次循环中将自身的threadLocals中的 ThreadLocal A 设置值成了“A-THREAD”,且后续没有remove(),然后在第5次循环中复用到了Thread t1,这时不需要进行任何处理通过 ThreadLocal A 调用 get() 拿到的就是“A-THREAD”,这肯定与预期的不相同,如下:

private static final ThreadLocal<String> A = ThreadLocal.withInitial(() -> "A");

private static final ThreadLocal<String> B = ThreadLocal.withInitial(() -> "B");

public void run1() {
    ThreadPoolUtils.execute(() -> {
       log.info("1、Child t1 {} thread value is : {}", Thread.currentThread().getName(), A.get()); //A
       log.info("2、Child t1 {} thread value is : {}", Thread.currentThread().getName(), B.get()); //B
       A.set("A-THREAD");
       log.info("3、Child t1 {} thread value is : {}", Thread.currentThread().getName(), A.get()); //A-THREAD
       log.info("4、Child t1 {} thread value is : {}", Thread.currentThread().getName(), B.get()); //B
    });
}

//在runTest方法中循环调用run1(),不间断创建线程池任务
public void runTest() throws InterruptedException {
    for (int i = 0; i < 100; i++) {
        run1();
        //主线程每隔1s调用一次
        TimeUnit.SECONDS.sleep(1);
    }
}

日志打印:

//线程 pool-1-thread-1 第一次输出
1Child t1 pool-1-thread-1 thread value is : A
2Child t1 pool-1-thread-1 thread value is : B
3Child t1 pool-1-thread-1 thread value is : A-THREAD
4Child t1 pool-1-thread-1 thread value is : B
//线程 pool-1-thread-1 第二次输出,可以看到差别
1Child t1 pool-1-thread-1 thread value is : A-THREAD
2Child t1 pool-1-thread-1 thread value is : B
3Child t1 pool-1-thread-1 thread value is : A-THREAD
4Child t1 pool-1-thread-1 thread value is : B    

如果在操作完成后依次调用remove()方法则不会出现上面的情况,如下:

public void run1() {
    ThreadPoolUtils.execute(() -> {
       log.info("Child t1 {} thread value is : {}", Thread.currentThread().getName(), A.get()); //A
       log.info("Child t1 {} thread value is : {}", Thread.currentThread().getName(), B.get()); //B
       A.set("A-THREAD");
       log.info("Child t1 {} thread value is : {}", Thread.currentThread().getName(), A.get()); //A-THREAD
       log.info("Child t1 {} thread value is : {}", Thread.currentThread().getName(), B.get()); //B
       //每次使用完调用remove方法,否则一定会造成取到旧值的情况
       A.remove();
       B.remove();
    });
}

日志打印:

//线程 pool-1-thread-1 第一次输出
1Child t1 pool-1-thread-1 thread value is : A
2Child t1 pool-1-thread-1 thread value is : B
3Child t1 pool-1-thread-1 thread value is : A-THREAD
4Child t1 pool-1-thread-1 thread value is : B
//线程 pool-1-thread-1 第二次输出
1Child t1 pool-1-thread-1 thread value is : A
2Child t1 pool-1-thread-1 thread value is : B
3Child t1 pool-1-thread-1 thread value is : A-THREAD
4Child t1 pool-1-thread-1 thread value is : B    

到这里我还在想着每次操作完之后调用remove(),把目前的值清空不就可以了。确保线程下次复用的时候值是预期的,但是在线程池的任务中,怎么尽量确保remove()方法会被调用呢?

  • 使用finally
public void run1() {
    ThreadPoolUtils.execute(() -> {
       try {
           A.set("A-THREAD");
           A.set("A-THREAD-01");
           log.info("Child t1 {} thread value is : {}", Thread.currentThread().getName(), A.get()); //A-THREAD
           log.info("Child t1 {} thread value is : {}", Thread.currentThread().getName(), B.get()); //B
       } finally {
           A.remove();
           B.remove();
       }
    });
}
  • 使用线程池钩子函数

在线程池类ThreadPoolExecutor中定义了钩子函数,可以在初始化或者任务执行完做特殊处理,在里面初始化ThreadLocal。重写beforeExecute()方法:

protected void beforeExecute(Thread t, Runnable r) { }

参考文章:在线程池中使用ThreadLocal,你必须要知道这一点


4.2 内存泄漏问题

ThreadLocalMap的数据源头 Entry[] table ,以及Entry类继承了WeakReference(四种引用之一:弱引用),在JVM自动GC的时候会对Key进行清理;

但是对于如果value是强引用类型,就需要手动调用remove(),否则在GC清理掉的Key的引用后,因为value可能是强引用类型,则没有被清理;

在使用者层面并不会再使用此value的引用,因为使用者层面不可能通过一个null key来找到value的,这样就会造成内存泄漏,即ThreadLocalMap中的 Entry[] table 会存在key为null,但是value不为null的entry项。

所以请在使用完ThreadLocal后请务必调用remove()方法,避免造成内存泄漏

例如:

try {
    //业务代码
} finally {
    threadLocal.remove();
}

下面是remove()方法的源码:

public void remove() {
    //(1) 获取当前线程绑定的threadLocals
    ThreadLocalMap m = getMap(Thread.currentThread());
    //(2) 如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量
    if (m != null)
        //(3) 如果map不为null,继续调用remove(this)方法
        m.remove(this);
}

查看3处调用的remove(this)方法:

private void remove(ThreadLocal<?> key) {
    //(1) 创建新都tab数组,引用指向当前ThreadLocal对象中的table
    Entry[] tab = table;
    //(2) tab的长度
    int len = tab.length;
    //(3) 根据当前ThreadLocal对象的唯一threadLocalHashCode值并通过与操作
    // 获取当前ThreadLocal对象在table中value所在的下标值i
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        //(4) 对table进行遍历,如果Entry所对应的key为当前ThreadLocal对象,执行clear方法
        if (e.get() == key) {
            //(5) 将引用置为null
            e.clear();
            //(6) 清除过时的条目,将当前Entry删除后,会继续循环往下检查是否有key为null的节点,如果有则一并删除,防止内存泄漏。
            expungeStaleEntry(i);
            return;
        }
    }
}

查看clear()方法,clear()方法位于Reference类中,由于Entry继承了WeakReference,此处的clear则是多态的体现

public void clear() {
    this.referent = null;
}

五、项目使用场景

精力有限,这里后续会持续更新补出一些项目中实际的案例

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
FastThreadLocal 是 Netty 中的一个优化版 ThreadLocal 实现。与 JDK 自带的 ThreadLocal 相比,FastThreadLocal 在性能上有所提升。 FastThreadLocal 的性能优势主要体现在以下几个方面: 1. 线程安全性:FastThreadLocal 使用了一种高效的方式来保证线程安全,避免了使用锁的开销,使得在高并发场景下性能更好。 2. 内存占用:FastThreadLocal 的内部数据结构更加紧凑,占用的内存更少,减少了对堆内存的占用,提高了内存的利用效率。 3. 访问速度:FastThreadLocal 在访问时,使用了直接索引的方式,避免了哈希表查找的开销,使得访问速度更快。 在 Netty 源码中,FastThreadLocal 主要被用于优化线程的局部变量存储,提高线程之间的数据隔离性和访问效率。通过使用 FastThreadLocal,Netty 在高性能的网络通信中能够更好地管理线程的局部变量,提供更高的性能和并发能力。 引用中提到的代码片段展示了 Netty 中的 InternalThreadLocalMap 的获取方式。如果当前线程是 FastThreadLocalThread 类型的线程,那么就直接调用 fastGet 方法来获取 InternalThreadLocalMap 实例;否则,调用 slowGet 方法来获取。 fastGet 方法中,会先尝试获取线程的 threadLocalMap 属性,如果不存在则创建一个新的 InternalThreadLocalMap,并设置为线程的 threadLocalMap 属性。最后返回获取到的 threadLocalMap。 slowGet 方法中,通过调用 UnpaddedInternalThreadLocalMap.slowThreadLocalMap 的 get 方法来获取 InternalThreadLocalMap 实例。如果获取到的实例为 null,则创建一个新的 InternalThreadLocalMap,并将其设置到 slowThreadLocalMap 中。最后返回获取到的 InternalThreadLocalMap。 综上所述,FastThreadLocal 是 Netty 中为了优化线程局部变量存储而设计的一种高性能的 ThreadLocal 实现。它通过减少锁的开销、优化内存占用和加快访问速度来提升性能。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [FastThreadLocal源码分析](https://blog.csdn.net/lvlei19911108/article/details/118021402)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [Netty 高性能之道 FastThreadLocal 源码分析(快且安全)](https://blog.csdn.net/weixin_33871366/article/details/94653953)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值