天天看点

ThreadLocal 原理 源码分析 [MD]

博文地址

我的GitHub 我的博客 我的微信 我的邮箱
baiqiantao bqt20094 [email protected]

目录

  • ThreadLocal
    • 总结
      • 使用线程池的问题
      • 内存泄漏问题
  • 源码分析
    • Thread
      • 构造方法
      • set() 方法
      • get() 方法
      • initialValue() 方法
    • ThreadLocalMap
      • 基础结构
      • Hash 算法
      • Hash 冲突
    • InheritableThreadLocal
      • 实现原理
      • 测试代码
    • 案例

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

    中就会出现

    key

    null

    Entry

    ,此后,这些

    key

    null

    Entry

    value

    就没有办法访问了。
  • 由于这些

    key

    null

    Entry

    value

    存在一条强引用链:

    Thread -> ThreaLocalMap -> Entry -> value

    ,这就造成这些

    value

    永远无法回收。
  • 如果当前线程迟迟不结束的话,就会造成内存泄漏。

如何解决内存泄漏的问题

在调用

ThreadLocal

get()、set()、remove()

的时候,都会主动清除掉

ThreadLocalMap

里所有

key

null

value

需要明确,使用

ThreadLocal

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

首先从

Thread

类源码入手:

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

Thread

类中有一个

threadLocals

和一个

inheritableThreadLocals

变量,它们都是

ThreadLocalMap

类型的变量。

默认情况下这两个变量都是 null,只有当前线程调用

ThreadLocal

类的

set

get

方法时才创建它们,实际上调用这两个方法的时候,最终调用的是

ThreadLocalMap

类对应的方法。

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

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 中找到自己存储的位置

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

方法可知,通过

ThreadLoca

set

方法存储的对象,都是存储在当前线程对象的

ThreadLocalMap

中的,其他线程访问不到,各个线程中通过

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()

方法指定的初始值。

核心逻辑依旧在

ThreadLocalMap

getEntry(tl)

方法中,具体逻辑同样在后面剖析

ThreadLocalMap

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

类似

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.

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 分布非常均匀。

HashMap

中解决冲突的方法是

链表法

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

ThreadLocalMap

开放地址法

(

线性探测法

):插入一个

Entry

时,如果通过

hash

计算的槽位中已经有了

Entry

数据,此时就会线性向后查找,一直找到

Entry

null

的槽位才会停止查找,并将当前元素放入此槽位中

ThreadLocalMap

set

数据分为以下几种情况:

  • 通过

    hash

    计算后的槽位对应的

    Entry

    数据为空:直接将数据放到该槽位即可
  • hash

    Entry

    数据不为空
    • 如果

      key

      值与当前

      ThreadLocal

      hash

      计算获取的

      key

      值一致:更新该槽位的数据即可
    • key

      ThreadLocal

      hash

      key

      值不一致:线性向后查找
      • 往后遍历过程中,在找到

        Entry

        null

        的槽位之前,没有遇到

        key

        过期的

        Entry

        :遍历散列数组,线性往后查找,如果找到

        Entry

        null

        的槽位,则将数据放入该槽位中,或者往后遍历过程中,遇到了

        key

        值相等的数据,直接更新即可
      • Entry

        null

        的槽位之前,遇到了

        key

        Entry

        :会进行一轮探测式清理操作,具体逻辑就不去理了,意义不大

set()

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

get

的逻辑和

set

类似,分为以下几种情况:

  • hash

    Entry.key

    和查找的

    key

    一致:则直接返回
  • hash

    Entry.key

    key

    不一致:则往后迭代查找,查找过程中也会进行一轮探测式清理操作

使用

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
           

2019-1-21

本文来自博客园,作者:白乾涛,转载请注明原文链接:https://www.cnblogs.com/baiqiantao/p/7257326.html

继续阅读