本文已收录到 GitHub AndroidFamily,有 Android 进阶常识体系,欢迎 Star。技术和职场问题,请关注大众号 [彭旭锐] 私信我提问。

前言

大家好,我是小彭。

在前面的文章里,咱们聊到了散列表的敞开寻址法和别离链表法,也聊到了 HashMap、LinkedHashMap 和 WeakHashMap 等根据别离链表法完结的散列表。

今日,咱们来评论 Java 规范库中一个运用敞开寻址法的散列表结构,也是 Java & Android “面试八股文” 的规范题库之一 —— ThreadLocal。

提示: 本文源码根据 Java 8 ThreadLocal。

  • 全网比较全的 ThreadLocal 原理具体解析 —— 原理篇
  • 全网比较全的 ThreadLocal 原理具体解析 —— 源码篇

学习路线图:

全网最全的 ThreadLocal 原理详细解析 —— 原理篇


1. 回忆散列表的作业原理

在开端剖析 ThreadLocal 的完结原理之前,咱们先回忆散列表的作业原理。

散列表是根据散列思想完结的 Map 数据结构,将散列思想应用到散列表数据结构时,便是经过 hash 函数提取键(Key)的特征值(散列值),再将键值对映射到固定的数组下标中,利用数组支撑随机拜访的特性,完结 O(1) 时间的存储和查询操作。

散列表示意图

全网最全的 ThreadLocal 原理详细解析 —— 原理篇

在从键值对映射到数组下标的过程中,散列表会存在 2 次散列抵触:

  • 第 1 次 – hash 函数的散列抵触: 这是一般意义上的散列抵触;
  • 第 2 次 – 散列值取余转数组下标: 本质上,将散列值转数组下标也是一次 Hash 算法,也会存在散列抵触。

事实上,因为散列表是压缩映射,所以咱们无法避免散列抵触,只能确保散列表不会因为散列抵触而失去正确性。常用的散列抵触解决办法有 2 类:

  • 敞开寻址法: 例如 ThreadLocalMap;
  • 别离链表法: 例如 HashMap。

敞开寻址(Open Addressing)的核心思想是: 在出现散列抵触时,在数组上重新勘探出一个闲暇位置。 经典的勘探办法有线性勘探、平方勘探和双散列勘探。线性勘探是最基本的勘探办法,咱们今日要剖析的 ThreadLocal 中的 ThreadLocalMap 散列表便是选用线性勘探的敞开寻址法。


2. 认识 ThreadLocal 线程部分存储

2.1 说一下 ThreadLocal 的特点?

ThreadLocal 供给了一种特别的线程安全办法。

运用 ThreadLocal 时,每个线程能够经过 ThreadLocal#getThreadLocal#set 办法拜访资源在当时线程的副本,而不会与其他线程产生资源竞赛。这意味着 ThreadLocal 并不考虑怎么解决资源竞赛,而是为每个线程分配独立的资源副本,从根本上避免产生资源抵触,是一种无锁的线程安全办法。

用一个表格总结 ThreadLocal 的 API:

public API 描绘
set(T) 设置当时线程的副本
T get() 获取当时线程的副本
void remove() 移除当时线程的副本
ThreadLocal<S> withInitial(Supplier<S>) 创立 ThreadLocal 并指定缺省值创立工厂
protected API 描绘
T initialValue() 设置缺省值

2.2 ThreadLocal 怎么完结线程阻隔?(要害理解)

ThreadLocal 在每个线程的 Thread 目标实例数据中分配独立的内存区域,当咱们拜访 ThreadLocal 时,本质上是在拜访当时线程的 Thread 目标上的实例数据,不同线程拜访的是不同的实例数据,因而完结线程阻隔。

Thread 目标中这块数据便是一个运用线性勘探的 ThreadLocalMap 散列表,ThreadLocal 目标自身就作为散列表的 Key ,而 Value 是资源的副本。当咱们拜访 ThreadLocal 时,便是先获取当时线程实例数据中的 ThreadLocalMap 散列表,再经过当时 ThreadLocal 作为 Key 去匹配键值对。

ThreadLocal.java

// 获取当时线程的副本
public T get() {
    // 先获取当时线程实例数据中的 ThreadLocalMap 散列表
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // 经过当时 ThreadLocal 作为 Key 去匹配键值对
    ThreadLocalMap.Entry e = map.getEntry(this);
    // 具体源码剖析见下文 ...
}
// 获取线程 t 的 threadLocals 字段,即 ThreadLocalMap 散列表
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
// 静态内部类
static class ThreadLocalMap {
    // 具体源码剖析见下文 ...
}

Thread.java

// Thread 目标的实例数据
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
// 线程退出之前,会置空threadLocals变量,以便随后GC
private void exit() {
    // ...
    threadLocals = null;
    inheritableThreadLocals = null;
    inheritedAccessControlContext = null;
    // ...
}

ThreadLocal 示意图

全网最全的 ThreadLocal 原理详细解析 —— 原理篇

2.3 运用 InheritableThreadLocal 承继父线程的部分存储

在事务开发的过程中,咱们可能希望子线程能够拜访主线程中的 ThreadLocal 数据,然而 ThreadLocal 是线程阻隔的,包括在父子线程之间也是线程阻隔的。为此,ThreadLocal 供给了一个相似的子类 InheritableThreadLocal,ThreadLocal 和 InheritableThreadLocal 分别对应于线程目标上的两块内存区域:

  • 1、ThreadLocal 字段: 在一切线程间阻隔;

  • 2、InheritableThreadLocal 字段: 子线程会承继父线程的 InheritableThreadLocal 数据。父线程在创立子线程时,会批量将父线程的有用键值对数据复制到子线程的 InheritableThreadLocal,因而子线程能够复用父线程的部分存储。

在 InheritableThreadLocal 中,能够重写 childValue() 办法修正复制到子线程的数据。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    // 参数:父线程的数据
    // 回来值:复制到子线程的数据,默以为直接传递
    protected T childValue(T parentValue) {
        return parentValue;
    }
}

需求特别留意:

  • 留意 1 – InheritableThreadLocal 区域在复制后依然是线程阻隔的: 在完结复制后,父子线程对 InheritableThreadLocal 的操作依然是彼此独立的。子线程对 InheritableThreadLocal 的写不会影响父线程的 InheritableThreadLocal,反之亦然;

  • 留意 2 – 复制过程在父线程履行: 这是简单混杂的点,尽管复制数据的代码写在子线程的结构办法中,可是依然是在父线程履行的。子线程是在调用 start() 后才开端履行的。

InheritableThreadLocal 示意图

全网最全的 ThreadLocal 原理详细解析 —— 原理篇

2.4 ThreadLocal 的主动整理与内存走漏问题

ThreadLocal 供给具有主动整理数据的能力,具体分为 2 个颗粒度:

  • 1、主动整理散列表: ThreadLocal 数据是 Thread 目标的实例数据,当线程履行完毕后,就会跟从 Thread 目标 GC 而被整理;

  • 2、主动整理无效键值对: ThreadLocal 是运用弱键的动态散列表,当 Key 目标不再被持有强引证时,垃圾收集器会按照弱引证战略主动收回 Key 目标,并在下次拜访 ThreadLocal 时整理无效键值对。

引证关系示意图

全网最全的 ThreadLocal 原理详细解析 —— 原理篇

然而,主动整理无效键值对会存在 “滞后性”,在滞后的这段时间内,无效的键值对数据没有及时收回,就产生内存走漏。

  • 举例 1: 假如创立 ThreadLocal 的线程一直继续运行,整个散列表的数据就会共同存在。比方线程池中的线程(大体)是复用的,这部分复用线程中的 ThreadLocal 数据就不会被整理;
  • 举例 2: 假如在数据无效后没有再拜访过 ThreadLocal 目标,那么自然就没有机会触发整理;
  • 举例 3: 即便拜访 ThreadLocal 目标,也纷歧定会触发整理(原因见下文源码剖析)。

综上所述:尽管 ThreadLocal 供给了主动整理无效数据的能力,可是为了避免内存走漏,在事务开发中应该及时调用 ThreadLocal#remove 整理无效的部分存储。

2.5 ThreadLocal 的运用场景

  • 场景 1 – 无锁线程安全: ThreadLocal 供给了一种特别的线程安全办法,从根本上避免资源竞赛,也体现了空间换时间的思想;

  • 场景 2 – 线程等级单例: 一般的单例目标是对整个进程可见的,运用 ThreadLocal 也能够完结线程等级的单例;

  • 场景 3 – 同享参数: 假如一个模块有十分多当地需求运用同一个变量,比较于在每个办法中重复传递同一个参数,运用一个 ThreadLocal 大局变量也是另一种传递参数办法。

2.6 ThreadLocal 运用示例

咱们选用 Android Handler 机制中的 Looper 音讯循环作为 ThreadLocal 的学习事例:

android.os.Looper.java

// /frameworks/base/core/java/android/os/Looper.java
public class Looper {
    // 静态 ThreadLocal 变量,大局同享同一个 ThreadLocal 目标
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        // 设置 ThreadLocal 变量的值,即设置当时线程相关的 Looper 目标
        sThreadLocal.set(new Looper(quitAllowed));
    }
    public static Looper myLooper() {
        // 获取 ThreadLocal 变量的值,即获取当时线程相关的 Looper 目标
        return sThreadLocal.get();
    }
    public static void prepare() {
        prepare(true);
    }
    ...
}

示例代码

new Thread(new Runnable() {
    @Override
    public void run() {
        Looper.prepare();
        // 两个线程独立拜访不同的 Looper 目标
        System.out.println(Looper.myLooper());
    }
}).start();
new Thread(new Runnable() {
    @Override
    public void run() {
        Looper.prepare();
        // 两个线程独立拜访不同的 Looper 目标
        System.out.println(Looper.myLooper());
    }
}).start();

要害如下:

  • 1、Looper 中的 ThreadLocal 被声明为静态类型,泛型参数为 Looper,大局同享同一个 ThreadLocal 目标;
  • 2、Looper#prepare() 中调用 ThreadLocal#set() 设置当时线程相关的 Looper 目标;
  • 3、Looper#myLooper() 中调用 ThreadLocal#get() 获取当时线程相关的 Looper 目标。

咱们能够画出 Looper 中拜访 ThreadLocal 的 Timethreads 图,能够看到不同线程独立拜访不同的 Looper 目标,即线程间不存在资源竞赛。

Looper ThreadLocal 示意图

全网最全的 ThreadLocal 原理详细解析 —— 原理篇

2.7 阿里巴巴 ThreadLocal 编程规约

在《阿里巴巴 Java 开发手册》中,亦有关于 ThreadLocal API 的编程规约:

  • 【强制】 SimpleDateFormate 是线程不安全的类,一般不要定义为 static ****变量。假如定义为 static,必须加锁,或者运用 DateUtils 东西类(运用 ThreadLocal 做线程阻隔)。

DataFormat.java

private static final ThreadLocal<DataFormat> df = new ThreadLocal<DateFormat>(){
    // 设置缺省值 / 初始值
    @Override
    protected DateFormat initialValue(){
        return new SimpleDateFormat("yyyy-MM-dd");
    }
};
// 运用:
DateUtils.df.get().format(new Date());
  • 【参考】 (原文过于啰嗦,以下是小彭翻译转述)ThreadLocal 变量主张运用 static 大局变量,能够确保变量在类初始化时创立,一切类实例能够同享同一个静态变量(例如,在 Android Looper 的事例中,ThreadLocal 便是运用 static 修饰的大局变量)。
  • 【强制】 必须收回自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常被反复用,假如不整理自定义的 ThreadLocal 变量,则可能会影响后续事务逻辑和造成内存走漏等问题。尽量在代码中运用 try-finally 块收回,在 finally 中调用 remove() 办法。

3. ThreadLocal 源码剖析

这一节,咱们来剖析 ThreadLocal 中主要流程的源码。

3.1 ThreadLocal 的特点

ThreadLocal 只要一个 threadLocalHashCode 散列值特点:

  • 1、threadLocalHashCode 相当于 ThreadLocal 的自定义散列值,在创立 ThreadLocal 目标时,会调用 nextHashCode() 办法分配一个散列值;

  • 2、ThreadLocal 每次调用 nextHashCode() 办法都会将散列值追加 HASH_INCREMENT,并记录在一个大局的原子整型 nextHashCode 中。

提示: ThreadLocal 的散列值序列为:0、HASH_INCREMENT、HASH_INCREMENT * 2、HASH_INCREMENT * 3、…

public class ThreadLocal<T> {
    // 疑问 1:OK,threadLocalHashCode 类似于 hashCode(),那为什么 ThreadLocal 不重写 hashCode()
    // ThreadLocal 的散列值,类似于重写 Object#hashCode()
    private final int threadLocalHashCode = nextHashCode();
    // 大局原子整型,每调用一次 nextHashCode() 累加一次
    private static AtomicInteger nextHashCode = new AtomicInteger();
    // 疑问:为什么 ThreadLocal 散列值的增量是 0x61c88647?
    private static final int HASH_INCREMENT = 0x61c88647;
    private static int nextHashCode() {
        // 回来上一次 nextHashCode 的值,并累加 HASH_INCREMENT
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
}
static class ThreadLocalMap {
    // 具体源码剖析见下文 ...
}

不出意外的话又有小朋友出来举手提问了‍♀️

  • ‍♀️疑问 1:OK,threadLocalHashCode 类似于 hashCode(),那为什么 ThreadLocal 不重写 hashCode()?

假如重写 Object#hashCode(),那么 threadLocalHashCode 散列值就会对一切散列表收效。而 threadLocalHashCode 散列值是专门针对数组为 2 的整数幂的散列表规划的,在其他散列表中纷歧定表现良好。因而 ThreadLocal 没有重写 Object#hashCode(),让 threadLocalHashCode 散列值只在 ThreadLocal 内部的 ThreadLocalMap 运用。

惯例做法

public class ThreadLocal<T> {
    // ThreadLocal 未重写 hashCode()
    @Override
    public int hashCode() {
        return threadLocalHashCode;
    }
}
  • ‍♀️疑问 2:为什么运用 ThreadLocal 作为散列表的 Key,而不是惯例思想用 Thread Id 作为 Key?

假如运用 Thread Id 作为 Key,那么就需求在每个 ThreadLocal 目标中保护散列表,而不是每个线程保护一个散列表。此刻,当多个线程并发拜访同一个 ThreadLocal 目标中的散列表时,就需求经过加锁确保线程安全。而 ThreadLocal 的计划让每个线程拜访独立的散列表,就能够从根本上规避线程竞赛。

3.2 ThreadLocal 的 API

剖析代码,能够总结出 ThreadLocal API 的用法和留意事项:

  • 1、ThreadLocal#get: 获取当时线程的副本;
  • 2、ThreadLocal#set: 设置当时线程的副本;
  • 3、ThreadLocal#remove: 移除当时线程的副本;
  • 4、ThreadLocal#initialValue: 由子类重写来设置缺省值:
    • 4.1 假如未射中(Map 取值为 nul),则会调用 initialValue() 创立并设置缺省值;
    • 4.2 ThreadLocal 的缺省值只会在缓存未射中时创立,即缺省值选用懒初始化战略;
    • 4.3 假如先设置后又移除副本,再次 get 获取副本未射中时依然会调用 initialValue() 创立并设置缺省值。
  • 5、ThreadLocal#withInitial: 便利设置缺省值,而不需求完结子类。

在 ThreadLocal 的 API 会经过 getMap() 办法获取当时线程的 Thread 目标中的 threadLocals 字段,这是线程阻隔的要害。

ThreadLocal.java

public ThreadLocal() {
    // do nothing
}
// 子类可重写此办法设置缺省值(办法命名为 defaultValue 获取更恰当)
protected T initialValue() {
    // 默认不供给缺省值
    return null;
}
// 协助办法:不重写 ThreadLocal 也能够设置缺省值
// supplier:缺省值创立工厂
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}
// 1. 获取当时线程的副本
public T get() {
    Thread t = Thread.currentThread();
    // ThreadLocalMap 具体源码剖析见下文
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 存在匹配的Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            T result = (T)e.value;
            return result;
        }
    }
    // 未射中,则获取并设置缺省值(即缺省值选用懒初始化战略)
    return setInitialValue();
}
// 获取并设置缺省值
private T setInitialValue() {
    T value = initialValue();
    // 其实源码中是并不是直接调用set(),而是复制了一份 set() 办法的源码
    // 这是为了避免子类重写 set() 办法后改动缺省值逻辑
    set(value);
    return value;
}
// 2. 设置当时线程的副本
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        // 直到设置值的时候才创立(即 ThreadLocalMap 选用懒初始化战略)
        createMap(t, value);
}
// 3. 移除当时线程的副本
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}
ThreadLocalMap getMap(Thread t) {
    // 要害:获取当时线程的 threadLocals 字段
    return t.threadLocals;
}
// ThreadLocal 缺省值协助类
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
    private final Supplier<? extends T> supplier;
    SuppliedThreadLocal(Supplier<? extends T> supplier) {
        this.supplier = Objects.requireNonNull(supplier);
    }
    // 重写 initialValue() 以设置缺省值
    @Override
    protected T initialValue() {
        return supplier.get();
    }
}

3.3 InheritableThreadLocal 怎么承继父线程的部分存储?

父线程在创立子线程时,在子线程的结构办法中会批量将父线程的有用键值对数据复制到子线程,因而子线程能够复用父线程的部分存储。

Thread.java

// Thread 目标的实例数据
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
// 结构办法
public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
    ...
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        // 复制父线程的 InheritableThreadLocal 散列表
        this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    ...
}

ThreadLocal.java

// 带 Map 的结构办法
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}
static class ThreadLocalMap {
    private ThreadLocalMap(ThreadLocalMap parentMap) {
        // 具体源码剖析见下文 ...
        Object value = key.childValue(e.value);
        ...
    }	
}

InheritableThreadLocal 在复制父线程散列表的过程中,会调用 InheritableThreadLocal#childValue() 测验转换为子线程需求的数据,默认是直接传递,能够重写这个办法修正复制的数据。

InheritableThreadLocal.java

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    // 参数:父线程的数据
    // 回来值:复制到子线程的数据,默以为直接传递
    protected T childValue(T parentValue) {
        return parentValue;
    }

下面,咱们来剖析 ThreadLocalMap 的源码。


后续源码剖析,见下一篇文章:全网比较全的 ThreadLocal 原理具体解析 —— 源码篇。


本文为稀土技术社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!

参考资料

  • 数据结构与算法剖析 Java 语言描绘(第 5 章 散列)—— [美] Mark Allen Weiss 著
  • 算法导论(第 11 章 散列表)—— [美] Thomas H. Cormen 等 著
  • 《阿里巴巴Java开发手册》 杨冠宝 编著
  • 数据结构与算法之美(第 18~22 讲) —— 王争 著,极客时间 出品
  • ThreadLocal 和 ThreadLocalMap源码剖析 —— KingJack 著
  • Why 0x61c88647? —— Dr. Heinz M. Kabutz 著

全网最全的 ThreadLocal 原理详细解析 —— 原理篇