End

ThreadLocal 原理 源码分析

本文地址


目录

ThreadLocal 原理 源码分析

ThreadLocal

ThreadLocal 为解决多线程并发问题提供了一种新的思路。使用它可以很简洁地编写出优美的多线程程序。当使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

使用场景

  • 存储单个线程上下文信息
  • 使变量线程安全:变量既然成为了每个线程内部的局部变量,自然就不会存在并发问题了
  • 减少参数传递

总结

ThreadLocal 提供了线程独有的局部变量(本地变量、副本变量),可以在整个线程存活的过程中随时取用,且线程之间互不干扰。

  • ThreadLocal 里 set 进去的数据,其实是存储在当前 Thread 里的(Thread 也是一个对象)
  • 每个 Thread 都有一个属性threadLocals,类型为ThreadLocal.ThreadLocalMap,它本质上是一个自定义的 map
  • 这个 map 的 entry 是ThreadLocal.ThreadLocalMap.Entry,这个 entry 继承自WeakReference<ThreadLocal<T>>
  • 这表明:ThreadLocal<?>被弱引用对象引用的对象ThreadLocal<T>(而非通过其 set 进去的数据)会在 gc 时被回收
  • 这个 map 其实只是一个数组,map 的 get/set 方法的参数(也即象征意义的 key)是ThreadLocal<?>
  • 数组中存储的值(也即象征意义的 value)即通过 ThreadLocal<?> 的 set 方法传过来的 Object(类型就是里面的泛型)
  • 由于 map 中对ThreadLocal<?>是通过弱引用的方式引用的,所以当ThreadLocal<?>不再被强引用时,此ThreadLocal<?>对象就会在 gc 时被回收

使用线程池的问题

使用线程池可以达到线程复用的效果,但是归还线程之前记得清除ThreadLocalMap,要不然再取出该线程的时候,ThreadLocal变量还会存在。

内存泄漏问题

为何存在内存泄漏的问题

  • ThreadLocalMap 中是以弱引用的方式引用的 ThreadLocal,如果一个 ThreadLocal 没有外部强引用来引用它,那么 GC 的时候,这个 ThreadLocal 就会被回收。
  • ThreadLocal 被回收后,ThreadLocalMap 中就会出现 keynullEntry,此后,这些 keynullEntryvalue 就没有办法访问了。
  • 由于这些 keynullEntryvalue 存在一条强引用链:Thread -> ThreaLocalMap -> Entry -> value,这就造成这些 value 永远无法回收。
  • 如果当前线程迟迟不结束的话,就会造成内存泄漏。

如何解决内存泄漏的问题

在调用 ThreadLocalget()、set()、remove()的时候,都会主动清除掉 ThreadLocalMap 里所有 keynullvalue

需要明确,使用 ThreadLocal 肯定是存在内存泄漏的问题的,上面的方案虽然解决了部分场景的内存泄漏问题,但并不彻底!

源码分析

Thread

首先从 Thread 类源码入手:

public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null; // 存储普通的线程数据(本地变量)
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; // 可以被子类继承
}

Thread 类中有一个 threadLocals 和一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量。

默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 setget 方法时才创建它们,实际上调用这两个方法的时候,最终调用的是ThreadLocalMap类对应的方法。

ThreadLocalMap 是 ThreadLocal 的内部类,本质是一个自定义的 map。

ThreadLocal

public ThreadLocal()
protected T initialValue()
public void set(T value)
public T get()
public void remove()

构造方法

private final int threadLocalHashCode = nextHashCode();  //用来在 map 中找到自己

public ThreadLocal() {}

ThreadLocal 实例的变量只有一个threadLocalHashCode,用来在 ThreadLocalMap 中找到自己存储的位置

set() 方法

public void set(T value) {
    Thread t = Thread.currentThread(); // 获取当前线程
    ThreadLocalMap map = t.threadLocals; // 返回当前线程中的成员变量 threadLocals
    if (map != null) map.set(this, value); // 注意是将该 ThreadLocal 实例作为key
    else t.threadLocals = new ThreadLocalMap(this, value); // 初始化线程中的成员变量,并赋值
}

set方法很简单,主要是判断ThreadLocalMap是否存在,然后使用ThreadLocalMap中的set方法进行数据处理。具体逻辑在后面剖析ThreadLocalMap源码时再看。

通过上面的set方法及下面get方法可知,通过ThreadLocaset方法存储的对象,都是存储在当前线程对象的ThreadLocalMap中的,其他线程访问不到,各个线程中通过get方法访问的是不同的对象。

get() 方法

public T get() {
    Thread t = Thread.currentThread(); // 获取当前线程
    ThreadLocalMap map = t.threadLocals; // 获取当前线程中的 ThreadLocalMap
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this); // 获取 ThreadLocal 对应的键值对
        if (e != null) return (T)e.value; // 如果键值对存在,则返回当前 ThreadLocal 对应的值
    }
    return setInitialValue(); // 如果键值对不存在,则返回 initialValue() 方法指定的初始值
}

If the variable has no value for the current thread, it is first initialized to the value returned by an invocation of the initialValue() method.

private T setInitialValue() {
    T value = initialValue(); // 获取指定的初始值
    set(value); // 中间这一块的逻辑和 set 方法完全一样
    return value;
}

get方法的逻辑也很简单,如果通过当前 ThreadLocal 能在 map 中找到非空的 Entry,则正常返回 Entry 中的值(也即之前通过 set 方法存储的值),否则返回 initialValue() 方法指定的初始值。

核心逻辑依旧在ThreadLocalMapgetEntry(tl)方法中,具体逻辑同样在后面剖析ThreadLocalMap源码时再看。

initialValue() 方法

initialValue() 方法仅在 get 方法中被调用,用于返回 ThreadLocal 对应的初始化值,一般作为匿名内部类使用。

关于 initialValue() 的注意事项:

  • Returns the current thread's "initial value" for this thread-local variable.
  • This method will be invoked the first time a thread accesses the variable with the get method, unless the thread previously invoked the set method, in which case the initialValue() method will not be invoked for the thread.
  • 注意这个方法可能不调用,也可能调用多次,不能在里面做初始化逻辑

一般没啥用的一个方法

ThreadLocalMap

ThreadLocalMap类似HashMap的结构,只是HashMap是由数组+链表实现的,而ThreadLocalMap中并没有链表结构。

ThreadLocalMap is a customized hash map suitable 适用于 only for maintaining 维护 thread local values.

基础结构

static class ThreadLocalMap {
    //The table, resized as necessary. table.length MUST always be a power of two.
    private Entry[] table; // 可自动扩容的数组

    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value; // The value associated with this ThreadLocal
        Entry(ThreadLocal<?> k, Object v) {
            super(k); // key
            value = v; // value
        }
    }
    //...
}
  • ThreadLocalMaps are constructed lazily, so we only create one when we have at least one entry to put in it.
  • To help deal with very large and long-lived 长期存在 usages, the hash table entries use WeakReferences for keys.
  • However, since ReferenceQueue are not used, stale 过时的 entries are guaranteed 确保 to be removed only when the table starts running out of space 没有空间.
  • Note that null keys mean that the key is no longer referenced, so the entry can be expunged 移除 from table.

Hash 算法

int i = key.threadLocalHashCode & (table.len-1); // 计算当前 key 在散列表中对应的数组下标位置

这里最关键的就是threadLocalHashCode值的计算,这个值其实在创建 ThreadLocal 时已经计算好了。

private final int threadLocalHashCode = nextHashCode(); // ThreadLocal 的 hash 值

private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT); // Atomically adds the given value to the current value.
}

private static AtomicInteger nextHashCode = new AtomicInteger(); //The next hash code to be given out. Updated atomically. Starts at zero.
private static final int HASH_INCREMENT = 0x61c88647;

每当创建一个ThreadLocal对象,这个ThreadLocal.nextHashCode 这个值就会在之前的基础上增长 0x61c88647。这个值很特殊,它是斐波那契数也叫黄金分割数,hash 增量为这个数字的好处就是:hash 分布非常均匀

Hash 冲突

HashMap中解决冲突的方法是链表法,冲突的数据挂载到链表上,如果链表长度超过一定数量则会转化成红黑树。

ThreadLocalMap中解决冲突的方法是开放地址法(线性探测法):插入一个Entry时,如果通过hash计算的槽位中已经有了Entry数据,此时就会线性向后查找,一直找到Entrynull的槽位才会停止查找,并将当前元素放入此槽位中

set() 方法

ThreadLocalMapset数据分为以下几种情况:

  • 通过hash计算后的槽位对应的Entry数据为空:直接将数据放到该槽位即可
  • 通过hash计算后的槽位对应的Entry数据不为空
    • 如果key值与当前ThreadLocal通过hash计算获取的key值一致:更新该槽位的数据即可
    • 如果key值与当前ThreadLocal通过hash计算获取的key值不一致:线性向后查找
      • 往后遍历过程中,在找到Entrynull的槽位之前,没有遇到key过期的Entry:遍历散列数组,线性往后查找,如果找到Entrynull的槽位,则将数据放入该槽位中,或者往后遍历过程中,遇到了key值相等的数据,直接更新即可
      • 往后遍历过程中,在找到Entrynull的槽位之前,遇到了key过期的Entry:会进行一轮探测式清理操作,具体逻辑就不去理了,意义不大

set()方法其实做了很多事情,包括:添加数据、更新数据、清理数据、优化数据桶的位置、数组扩容,具体逻辑就不去理了,意义不大。

get() 方法

get的逻辑和set类似,分为以下几种情况:

  • 通过hash计算后的槽位对应的Entry.key和查找的key一致:则直接返回
  • 通过hash计算后的槽位对应的Entry.key和查找的key不一致:则往后迭代查找,查找过程中也会进行一轮探测式清理操作

InheritableThreadLocal

使用ThreadLocal的时候,在异步场景下是无法给子线程共享父线程中创建的线程副本数据的。使用InheritableThreadLocal便可以解决这个问题。

实现原理

实现原理很简单:在父线程中通过new Thread()创建子线程时,Thread#init方法会在Thread的构造方法中被调用,在init方法中会拷贝父线程的InheritableThreadLocal中的数据到子线程中:

private void init(...boolean inheritThreadLocals) {
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}

测试代码

ThreadLocal<String> threadLocal = new ThreadLocal<>();
ThreadLocal<String> local = new InheritableThreadLocal<>();
threadLocal.set("白乾涛");
local.set("白乾涛");

ThreadLocal<String> threadLocal2 = ThreadLocal.withInitial(() -> "白乾涛2");
ThreadLocal<String> local2 = InheritableThreadLocal.withInitial(() -> "白乾涛2");

new Thread(() -> {
    System.out.println("子线程获取父线程ThreadLocal数据:" + threadLocal.get() + "-" + threadLocal2.get()); // null-白乾涛2
    System.out.println("子线程获取父线程InheritableThreadLocal数据:" + local.get() + "-" + local2.get()); // 白乾涛-白乾涛2
}).start();

案例

public class Test {
    ThreadLocal<String> mNameLocal = new ThreadLocal<>();
    ThreadLocal<Long> mIdLocal = ThreadLocal.withInitial(() -> {
        System.out.println(Thread.currentThread().getName() + " 调用了 get");
        return -1L; //初始化数据
    });

    public static void main(String[] args) throws InterruptedException {
        new Test().test();
    }

    private void test() throws InterruptedException {
        mNameLocal.set(Thread.currentThread().getName()); //更新【主线程】数据
        mIdLocal.set(Thread.currentThread().getId()); //更新【主线程】数据

        Thread thread = new Thread(() -> {
            mNameLocal.set(Thread.currentThread().getName()); //更新【子线程】数据
            //mIdLocal.set(Thread.currentThread().getId()); //更新【子线程】数据
            System.out.println(mNameLocal.get() + " " + mIdLocal.get()); //Thread-0 -1
        });
        thread.start();
        thread.join(); //效果等同于同步:等 thread 执行完毕后再执行下面的逻辑

        System.out.println(mNameLocal.get() + " " + mIdLocal.get()); //main 1
        mNameLocal.remove();
        mIdLocal.remove();
        System.out.println(mNameLocal.get() + " " + mIdLocal.get()); //null -1
    }
}

打印结果:

Thread-0 调用了 get
Thread-0 -1
main 1
main 调用了 get
null -1

2017-07-29

posted @ 2017-07-29 20:11  白乾涛  阅读(13961)  评论(0编辑  收藏  举报