ThreadLocal

1. ThreadLocal 是什么

ThreadLocal 用于解决多线程环境下的线程安全问题,ThreadLocal 有两个使用场景:

  1. 线程封闭: ThreadLoal 为每个线程创建一个独享的变量副本 (线程间数据隔离),每个线程只能修改自己所持有的变量副本而不会影响其他线程的变量副本,确保了线程安全;
  2. 上下文传递: ThreadLocal 保存的信息,可以被同一个线程内的不同方法访问,可以使用 ThreadLocal 传递上下文信息,减少方法之间的传参 (类似于全局变量);

2. ThreadLocal 如何使用

API 描述
ThreadLocal() 构造函数,创建ThreadLocal对象
get():T 获取当前线程绑定的局部变量
set(value:T) 设置当前线程绑定的局部变量
remove() 移除当前线程绑定的局部变量

使用案例

java 复制代码
public class ThreadLocalSimple {

    private static ThreadLocal<String> sThreadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        // 1.开启五个线程,同时向 sThreadLocal 中设置不同的值
        // 2.随后从 sThreadLocal 中读取设置的数据
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(() -> {
                String threadName = Thread.currentThread().getName();
                // 将当前线程的名称设置到 sThreadLocal 中
                sThreadLocal.set(Thread.currentThread().getName());
                System.out.println(threadName + "-> getFromThreadLocal:" + sThreadLocal.get());
            });
            thread.setName("thread-" + i);
            thread.start();
        }
    }

}

// 输出
thread-2-> getFromThreadLocal:thread-2
thread-4-> getFromThreadLocal:thread-4
thread-3-> getFromThreadLocal:thread-3
thread-1-> getFromThreadLocal:thread-1
thread-0-> getFromThreadLocal:thread-0

从结果来看可以发现,每个线程都能够从同一个对象 sThreadLocal 中正确的获取到设置的内容;

3. ThreadLocalSynchronized 关键字

ThreadLocal 模式和 Synchronized 关键字都是用于处理多线程并发访问变量的问题,但是两者处理问题的方式不同,具体如下:

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

简单来说

  • Synchronized 通过锁机制让不同的线程排队访问同一个变量,来保证线程安全问题,用于线程间的数据共享问题;
  • ThreadLocal 为每个线程创建一个变量副本,每个线程访问自己的变量副本,来保证每个线程访问的数据不相干扰,用于线程间的数据隔离;

4. Java 中的引用

Java中的引用类型有四种,根据引用强度的由强到弱,分别是: 强引用、软引用、弱引用、虚引用;

引用类型 GC回收时机 使用示例
强引用 Strong Reference 如果一个对象具有强引用,那垃圾回收器绝不会回收该对象 Object obj = new Object();
软引用 Soft Reference 在内存不足时,会对软引用对象进行回收 SoftReference<Object> softObj = new SoftReference();
弱引用 Weak Reference 第一次 GC 回收时,如果垃圾回收器遍历到此弱引用,则将该弱引用对象回收 WeakReference<Object> weakObj = new WeakReference();
虚引用 Phantom Reference 一个对象是否有虚引用的存在,不会对其生存的时间产生影响,同时也无法从一个虚引用来获取一个对象的实例 不会使用

5. ThreadTheadLocalMapThreadLocal 之间的关系

JDK1.8 开始,ThreadLocal 的内部结构改成: 每个 Thread 维护一个 ThreadLocalMap, 这个 map 存储的 keyThreadLocal 对象,value 为真正要存储的值,具体如下:

  1. 每个 Thread 线程内部都有一个 ThreadLocalMap 类型的变量 thread.threadLocals 默认为null;
  2. ThreadLocalMap 存储的键值对分别为 ThreadLocal (key) 和 线程的变量副本 (value);
  3. Thread 内部的 ThreadLocalMap 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 map 获取和设置线程的变量值;
  4. 对于不同的线程,通过 ThreadLocal (key),获取到的只能是线程自己维护的变量副本,从而实现副本数据的相互隔离;

为什么是这个结构

  1. 实际开发过程中,Thread 的数量往往会多于 ThreadLocal 的数量,这样设计后会减少 Entry 的数量;
  2. Thread 结束后,对应的 ThreadLocalMap 也会随之销毁;

6. ThreadLocal 源码分析

6.1 常用函数解析

6.1.1 void set(T value)

设置当前线程对应的ThreadLocal的值,value 为要保存在当前线程中的值,流程如下:

  1. 获取当前线程 Thread;
  2. 获取当前线程对应的 ThreadLocalMap 对象 threadLocals;
    • threadLocals != null: 将参数 value 存储到 map 中,keyThreadLocal;
    • threadLocals == null: 创建 ThreadLocalMap 对象,并赋初始值 value;
java 复制代码
// java.lang.ThreadLocal

public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取线程 t 对应的 ThreadLocalMap 对象
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 成功获取到 map 对象时,保存对应的键值对
        map.set(this, value);
    } else {
        // 首次访问 map 为null,走map创建流程
        createMap(t, value);
    }
}

// 返回线程t对应的 ThreadLocalMap 对象,首次获取返回 null
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// 为线程 t 创建 ThreadLocalMap 对象,并赋初始值 firstValue
void createMap(Thread t, T firstValue) {
    // 这里的 this 表示 ThreadLocal 对象
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

6.1.2 T get()

  1. 获取当前线程 Thread;
  2. 获取当前线程对应的 ThreadLocalMap 对象 threadLocals;
  3. 获取 key 为当前ThreadLocal 对象的 value 值; (假设: threadLocals != null)
  4. 返回对应的 value;(假设 ThreadLocal 对应的 value != null)

上述 2、3两步失败时,则会触发 ThreadLocal.initialValue() 函数,并根据默认的初始值更新 ThreadLocal 对应的 value,最后返回初始值;

java 复制代码
// java.lang.ThreadLocal

// 返回当前线程中保存 ThreadLocal 的值
public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取线程 t 对应的 ThreadLocalMap 对象
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 成功获取到 ThreadLocalMap 对象时,根据key (ThreadLocal) 获取存储的value值
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            // 成功获取到key (ThreadLocal) 存储的value时,返回对应值
            T result = (T)e.value;
            return result;
        }
    }
    // 1. 线程 t 还未创建 ThreadLocalMap 对象
    // 2. ThreadLocalMap 没有成功获取到 ThreadLocal key 对应的value时
    // 走下面的初始化流程
    return setInitialValue();
}

// 初始化ThreadLocal并返回初始化的值
private T setInitialValue() {
    // 调用 ThreadLocal 的初始化函数
    T value = initialValue();
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取线程 t 对应的 ThreadLocalMap 对象
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // map存在,存储初始值value
        map.set(this, value);
    } else {
        // map不存在,创建ThreadLocalMap并存储初始值value
        createMap(t, value);
    }
    // 返回初始值
    return value;
}

6.1.3 void remove()

删除当前线程存储的 ThreadLocal 对应的 Entry;

java 复制代码
// java.lang.ThreadLocal

public void remove() {
    // 获取当前线程对应的 ThreadLocalMap 对象
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null) {
        // 删除key为当前 ThreadLocal 的对应的 entry
        m.remove(this);
    }
}

6.1.4 protected T initialValue()

返回当前线程对应的 ThreadLocal的初始值,此方法的第一次调用发生在,线程通过 get() 方法访问此线程的 ThreadLocal 值时,如果该线程先调用了 set() 方法,那么理论上 initialValue() 方法不会被触发;

java 复制代码
// java.lang.ThreadLocal

protected T initialValue() {
    // 默认返回 null,如果需要返回默认值时,需要子类重写该方法
    return null;
}

6.2 ThreadLocalMap

可以看到 ThreadLocal 的操作实际上是围绕 ThreadLocalMap 对象展开的

6.2.1 ThreadLocalMap 的基本结构

ThreadLocalMapThreadLocal 的静态内部类,没有实现 Map 接口,用独立的方式实现了 Map 的功能,结构图如下:

ThreadLocalMapkey-value 封装在 Entry 对象中,然后将 Entry 对象存储在类型为 Entry[] 的成员变量 table 中;

6.2.2 关于 Entry

ThreadLocalMap 存储的 EntryThreadLocalMap 的静态内部类,继承自 WeakReference 弱引用类,同时限定了弱引用持有的对象类型为 ThreadLocal<?> 类型;

Entry 存储的 key-value 结构中,弱引用持有 key (也就是ThreadLocal) 对象,其目的是将 ThreadLocal 对象的生命周期和线程的生命周期解绑;

java 复制代码
// java.lang.ThreadLocal

// 限定了弱引用的数据类型为 ThreadLocal
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;

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

6.2.3 ThreadLocalMap 的创建

ThreadLocal 的常用函数可以发现,Thread 并没有在初始化的时候创建 ThreadLocalMap 对象,而是在 ThreadLocal 调用 set()/get() 时,通过 createMap(Thread, T) 创建;

java 复制代码
// java.lang.ThreadLocal

// 为线程 t 创建 ThreadLocalMap 对象并设置第一个值 firstValue
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

static class ThreadLocalMap {
    // 初始容量 (注意: 容量大小需要为2的幂次方)
    private static final int INITIAL_CAPACITY = 16;

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

    // 数组里面真是存储的数据数量,可用于判断table当前使用量是否达到了阈值
    private int size = 0;

    // table用于判断是否需要扩容的阈值
    private int threshold; // Default to 0

    // 构造函数
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        // 默认创建一个初始容量为16的 Entry 数组
        table = new Entry[INITIAL_CAPACITY];
        // 计算key的哈希值,然后通过 & 运算得到一个 [0, INITIAL_CAPACITY - 1] 区间的数字
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        // 使用上述计算出的index,存储 key-value 键值对对象 Entry
        table[i] = new Entry(firstKey, firstValue);
        // 更新 size 大小
        size = 1;
        // 更新默认的阈值
        setThreshold(INITIAL_CAPACITY);
    }
    ...
}

ThreadLocalMap 构造函数首先创建了一个长度为 16 的 Entry 数组,然后通过 firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1) 计算得到一个 [0, table.length - 1] 区间内的 index 下标 (这里 table.length = 16);

6.2.4 Entry[] 的 index 计算

ThreadLocalMap 的构造函数中可以看到,Entry[] 通过 firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1) 计算出 EntryEntry[] 中的 index;

这里的 firstKey 就是 ThreadLocal 对象,threadLocalHashCode 为一个 16 进制的数值,默认值为常量 HASH_INCREMENT = 0x61c88647;

java 复制代码
// java.lang.ThreadLocal

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

private final int threadLocalHashCode = nextHashCode();

private static int nextHashCode() {
    //  nextHashCode 默认为0,因此返回的初始值为 0x61c88647 
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

// 创建一个原子操作的 Integer 类,初始值为 0
private static AtomicInteger nextHashCode = new AtomicInteger();
  • & (INITIAL_CAPACITY - 1)

计算 hash 的时候采用 hashCode & (table.length - 1) 的算法,相当于取模运算(hashCode % table.length)得到一个 [0, table.length - 1] 区间内的值;

6.2.5 ThreadLocalMap 添加 Entry

ThreadLocalthreadLocalHashCode 字段会通过静态函数 nextHashCode() 获取到一个 16 进制数值,由于 nextHashCode() 是一个静态函数,且每次调用时都会执行一次 add 操作,因此每个 ThreadLocal 对象获取到的 threadLocalHashCode 为不同的 16 进制数值;

ThreadLocalMap 添加新的 key-value 之前,会先通过 ThreadLocal.threadLocalHashCode 计算出 key 对应 table[]index ,这里会有几种情况:

  1. table[index] != null && table[index].key == key: index 对应位置已经存在 entry 对象,且 entry.key 对象和要存储的 key 是同一个对象,此时直接将新的 value 赋值给 entry.value;
  2. table[index] != null && table[index].key == null: index 对应位置已经存在 entry 对象,但是 entry.key 已经被回收,此时 index 位置可以被重用;
  3. table[index] != null && table[index].key != null: 表示该位置已经存储了一个有效的 entry (哈希冲突),通过 nextIndex(index, len) 向后推一个位置,此时有两种情况:
    • index + 1 < len: 此时新的 index 没有数组越界,使用新的 index 执行 步骤1和2的判断;
    • index + 1 = len: 此时新的 index 发生了数组越界,此时返回 0 值,执行步骤1和2的判断;
  4. table[index] == null: 表示该位置是一个还未被使用的 index ,此时直接向该位置插入新的 Entry 对象;
java 复制代码
// java.lang.ThreadLocal

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    // 通过key的哈希值计算出当前key在 table 数组中的 index
    int i = key.threadLocalHashCode & (len-1);

    // 由于哈希冲突的存在,不同key会计算出相同的 index
    // 这里 for循环从 index = i 位置开始遍历取出缓存的 Entry 对象
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        // 获取到table数组中缓存的 Entry 的 key (ThreadLocal)
        ThreadLocal<?> k = e.get();

        if (k == key) {
            // 如果当前 index 缓存的 entry.key 跟需要设置的 ThreadLocal 相等,直接更新 entry 对象的 value
            e.value = value;
            return;
        }

        if (k == null) {
            // key 为弱引用对象,key == null 说明原先的 ThreadLocal 被回收了
            // 此时 table[i] 位置可以被当前 key-value 复用
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // index = i 位置不存在 entry 或者没有可以复用的 index,直接创建一个 Entry 对象
    tab[i] = new Entry(key, value);
    // 更新 entry 数量
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        // 扩容
        rehash();
}

7. ThreadLocal 中的内存泄露问题

ThreadThreadLocalThreadLocalMap 之间的引用关系如图:

内存泄露原因

从引用关系图可以看到 ThreadLocalMap 作为 Thread 的属性,其生命周期是跟 Thread 一样长,假设 ThreadLocal 被回收,而线程还未结束,那么 ThreadLocalMap 中对应的 Entry.key 会被置为 null,此时这个 entry.value 在线程生命周期内不会再次被访问,如果线程是复用的,那么该 ThreadLocalMap 内部就会存在一个或多个 entry(null, value) 对象,从而导致内存泄漏;

如何解决

  1. ThreadLocal 对象声明为 static ,让 ThreadLcoal 的生命周期更长,此时就会一直存在强引用;
  2. 每次使用完 ThreadLocal 对象,及时的调用 ThreadLocal.remove() 方法清除数据;
相关推荐
zhangphil15 分钟前
Android绘图Path基于LinearGradient线性动画渐变,Kotlin(2)
android·kotlin
watl043 分钟前
【Android】unzip aar删除冲突classes再zip
android·linux·运维
键盘上的蚂蚁-1 小时前
PHP爬虫类的并发与多线程处理技巧
android
喜欢猪猪2 小时前
Java技术专家视角解读:SQL优化与批处理在大数据处理中的应用及原理
android·python·adb
JasonYin~3 小时前
HarmonyOS NEXT 实战之元服务:静态案例效果---手机查看电量
android·华为·harmonyos
zhangphil3 小时前
Android adb查看某个进程的总线程数
android·adb
抛空4 小时前
Android14 - SystemServer进程的启动与工作流程分析
android
Gerry_Liang6 小时前
记一次 Android 高内存排查
android·性能优化·内存泄露·mat
天天打码7 小时前
ThinkPHP项目如何关闭runtime下Log日志文件记录
android·java·javascript
爱数学的程序猿10 小时前
Python入门:6.深入解析Python中的序列
android·服务器·python