关于程序员:ThreadLocal原理以及使用

39次阅读

共计 7825 个字符,预计需要花费 20 分钟才能阅读完成。

ThreadLocal 原理以及应用

TL 简介

当多线程访问共享可变数据时,波及到线程间同步的问题,并不是所有时候,都要用到共享数据,所以就须要线程关闭出场了。数据都被关闭在各自的线程之中,就不须要同步,这种通过将数据关闭在线程中而防止应用同步的技术称为 线程关闭。本文次要介绍线程关闭中的其中一种体现:ThreadLocal,将会介绍什么是 ThreadLocal;从 ThreadLocal 源码角度剖析,最初介绍 ThreadLocal 的利用场景。

什么是 ThreadLocal

ThreadLocal 是 Java 里一种非凡变量,它是一个线程级别变量,每个线程都有一个 ThreadLocal 就是每个线程都领有了本人独立的一个变量,竞态条件被彻底消除了,在并发模式下是相对平安的变量。

能够通过 ThreadLocal<T> value = new ThreadLocal<T>(); 来应用,它是反对泛型的。

它会主动在每一个线程上创立一个 T 的正本,正本之间彼此独立,互不影响,能够用 ThreadLocal 存储一些参数,以便在线程中多个办法中应用,用以代替办法传参的做法。

根本案例

上面是 ThreadLocal 的根本应用案例,也就是线程扭转的只是本人的一个变量,不影响其余线程。

public class ThreadLocalTest {

    /**
     * ThreadLocal 变量,每个线程都有一个正本,互不烦扰
     */
    public static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

    public static void main(String[] args) throws Exception {new ThreadLocalTest().threadLocalTest();}

    public void threadLocalTest() throws Exception {
        // 主线程设置值
        THREAD_LOCAL.set("init value");
        String v = THREAD_LOCAL.get();
        System.out.println("Thread- 0 线程执行之前," + Thread.currentThread().getName() + "线程取到的值:" + v);

        new Thread(new Runnable() {
            @Override
            public void run() {String v = THREAD_LOCAL.get();
                System.out.println(Thread.currentThread().getName() + "线程取到的值:" + v);
                // 设置 threadLocal
                THREAD_LOCAL.set("new value");
                v = THREAD_LOCAL.get();
                System.out.println("从新设置之后," + Thread.currentThread().getName() + "线程取到的值为:" + v);
                System.out.println(Thread.currentThread().getName() + "线程执行完结");
            }
        }).start();
        // 期待所有线程执行完结
        Thread.sleep(3000L);
        v = THREAD_LOCAL.get();
        System.out.println("Thread- 0 线程执行之后," + Thread.currentThread().getName() + "线程取到的值:" + v);
    }
}

首先通过 static final 定义了一个 THREAD_LOCAL 变量,其中 static 是为了确保全局只有一个保留 String 对象的 ThreadLocal 实例;final 确保 ThreadLocal 的实例不可更改,避免被意外扭转,导致放入的值和取出来的不统一,另外还能避免 ThreadLocal 的内存透露。下面的例子是演示在不同的线程中获取它会失去不同的后果,运行后果如下:

Thread- 0 线程执行之前,main 线程取到的值:init value
Thread- 0 线程取到的值:null
从新设置之后,Thread- 0 线程取到的值为:new value
Thread- 0 线程执行完结
Thread- 0 线程执行之后,main 线程取到的值:init value

案例的整体执行流程

首先在 Thread-0 线程执行之前,先给 THREAD_LOCAL 设置为 init value,而后能够取到这个值,而后通过创立一个新的线程当前去取这个值,发现新线程取到的为 null,意外着这个变量在不同线程中取到的值是不同的,不同线程之间对于 ThreadLocal 会有对应的正本,接着在线程 Thread-0 中执行对 THREAD_LOCAL 的批改,将值改为 new value,能够发现线程 Thread-0 获取的值变为了 new value,主线程仍然会读取到属于它的正本数据 init value`,这就是线程的关闭。

ThreadLocal 源码剖析

ThreadLocal 中的重要属性

// 以后 ThreadLocal 的 hashCode,由 nextHashCode() 计算而来,用于计算以后 ThreadLocal 在 ThreadLocalMap 中的索引地位
private final int threadLocalHashCode = nextHashCode();
// 哈希魔数,次要与斐波那契散列法以及黄金分割无关
private static final int HASH_INCREMENT = 0x61c88647;
// 返回计算出的下一个哈希值,其值为 i * HASH_INCREMENT,其中 i 代表调用次数
private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// 保障了在一台机器中每个 ThreadLocal 的 threadLocalHashCode 是惟一的
private static AtomicInteger nextHashCode = new AtomicInteger();

其中的 HASH_INCREMENT 也不是轻易取的,它转化为十进制是 16405315272654435769 转换成 int 类型就是 -16405315272654435769 等于 (√5-1)/2 乘以 2 的 32 次方。(√5-1)/2 就是黄金分割数,近似为 0.618,也就是说 0x61c88647 了解为一个黄金分割数乘以 2 的 32 次方,它能够保障 nextHashCode 生成的哈希值,平均的散布在 2 的幂次方上,且小于 2 的 32 次方。

ThreadLocalMap

除了上述属性外,还有一个重要的属性 ThreadLocalMap,ThreadLocalMap 是 ThreadLocal 的动态外部类,当一个线程有多个 ThreadLocal 时,须要一个容器来治理多个 ThreadLocal,ThreadLocalMap 的作用就是治理线程中多个 ThreadLocal,源码如下:

static class ThreadLocalMap {
    /**
     * 键值对实体的存储构造
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        // 以后线程关联的 value,这个 value 并没有用弱援用追踪
        Object value;

        /**
         * 结构键值对
         *
         * @param k k 作 key, 作为 key 的 ThreadLocal 会被包装为一个弱援用
         * @param v v 作 value
         */
        Entry(ThreadLocal<?> k, Object v) {super(k);
            value = v;
        }
    }

    // 初始容量,必须为 2 的幂
    private static final int INITIAL_CAPACITY = 16;

    // 存储 ThreadLocal 的键值对实体数组,长度必须为 2 的幂
    private Entry[] table;

    // ThreadLocalMap 元素数量
    private int size = 0;

    // 扩容的阈值,默认是数组大小的三分之二
    private int threshold;
}

从源码中看到 ThreadLocalMap 其实就是一个简略的 Map 构造,底层是数组,有初始化大小,也有扩容阈值大小,数组的元素是 Entry,Entry 的 key 就是 ThreadLocal 的援用,value 是 ThreadLocal 的值 。ThreadLocalMap 解决 hash 抵触的形式采纳的是 线性探测法,如果发生冲突会持续寻找下一个空的地位。

这样的就有可能会产生内存透露的问题。

ThreadLocal 内存透露

ThreadLocal 在没有内部强援用时,产生 GC 时会被回收,那么 ThreadLocalMap 中保留的 key 值就变成了 null,而 Entry 又被 threadLocalMap 对象援用,threadLocalMap 对象又被 Thread 对象所援用,那么当 Thread 始终不终结的话,value 对象就会始终存在于内存中,也就导致了内存透露,直至 Thread 被销毁后,才会被回收。

那么如何防止内存透露呢?

在应用完 ThreadLocal 变量后,须要咱们手动 remove 掉,避免 ThreadLocalMap 中 Entry 始终放弃对 value 的强援用,导致 value 不能被回收,其中 remove 源码如下所示:

/**
 * 清理以后 ThreadLocal 对象关联的键值对
 */
public void remove() {
    // 返回以后线程持有的 map
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null) {
        // 从 map 中清理以后 ThreadLocal 对象关联的键值对
        m.remove(this);
    }
}

remove 办法的时序图如下所示:

remove 办法是先获取到以后线程的 ThreadLocalMap,并且调用了它的 remove 办法,从 map 中清理以后 ThreadLocal 对象关联的键值对,这样 value 就能够被 GC 回收了.

ThreadLocal 的 set 办法

TL 实现线程隔离的次要都是在 set 办法中实现的。

ThreadLocal 的 set 办法,源码如下:

/**
 * 为以后 ThreadLocal 对象关联 value 值
 *
 * @param value 要存储在此线程的线程正本的值
 */
public void set(T value) {
    // 返回以后 ThreadLocal 所在的线程
    Thread t = Thread.currentThread();
    // 返回以后线程持有的 map
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 如果 ThreadLocalMap 不为空,则间接存储 <ThreadLocal, T> 键值对
        map.set(this, value);
    } else {
        // 否则,须要为以后线程初始化 ThreadLocalMap,并存储键值对 <this, firstValue>
        createMap(t, value);
    }
}

set 办法的作用是把咱们想要存储的 value 给保留进去。set 办法的流程次要是:

  • 先获取到以后线程的援用
  • 利用这个援用来获取到 ThreadLocalMap
  • 如果 map 为空,则去创立一个 ThreadLocalMap
  • 如果 map 不为空,就利用 ThreadLocalMap 的 set 办法将 value 增加到 map 中

set 办法的时序图如下所示:

其中 map 就是咱们下面讲到的 ThreadLocalMap,能够看到它是通过以后线程对象获取到的 ThreadLocalMap,接下来咱们看 getMap 办法的源代码:

/**
 * 返回以后线程 thread 持有的 ThreadLocalMap
 *
 * @param t 以后线程
 * @return ThreadLocalMap
 */
ThreadLocalMap getMap(Thread t) {return t.threadLocals;}

getMap 办法的作用次要是获取以后线程内的 ThreadLocalMap 对象,原来这个 ThreadLocalMap 是线程的一个属性,上面让咱们看看 Thread 中的相干代码:

/**
 * ThreadLocal 的 ThreadLocalMap 是线程的一个属性,所以在多线程环境下 threadLocals 是线程平安的
 */
ThreadLocal.ThreadLocalMap threadLocals = null;

能够看出每个线程都有 ThreadLocalMap 对象,被命名为 threadLocals,默认为 null,所以每个线程的 ThreadLocals 都是隔离独享的。

调用 ThreadLocalMap.set() 时,会把以后 threadLocal 对象作为 key,想要保留的对象作为 value,存入 map。

其中 ThreadLocalMap.set() 的源码如下:

/**
 * 在 map 中存储键值对 <key, value>
 *
 * @param key   threadLocal
 * @param value 要设置的 value 值
 */
private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;
    int len = tab.length;
    // 计算 key 在数组中的下标
    int i = key.threadLocalHashCode & (len - 1);
    // 遍历一段间断的元素,以查找匹配的 ThreadLocal 对象
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        // 获取该哈希值处的 ThreadLocal 对象
        ThreadLocal<?> k = e.get();

        // 键值 ThreadLocal 匹配,间接更改 map 中的 value
        if (k == key) {
            e.value = value;
            return;
        }

        // 若 key 是 null,阐明 ThreadLocal 被清理了,间接替换掉
        if (k == null) {replaceStaleEntry(key, value, i);
            return;
        }
    }

    // 直到遇见了空槽也没找到匹配的 ThreadLocal 对象,那么在此空槽处安顿 ThreadLocal 对象和缓存的 value
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 如果没有元素被清理,那么就要查看以后元素数量是否超过了容量阙值(数组大小的三分之二),以便决定是否扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold) {
        // 扩容的过程也是对所有的 key 从新哈希的过程
        rehash();}
}

ThreadLocal 的 get 办法

理解完 set 办法后,让咱们看下 get 办法,源码如下:

/**
 * 返回以后 ThreadLocal 对象关联的值
 *
 * @return
 */
public T get() {
    // 返回以后 ThreadLocal 所在的线程
    Thread t = Thread.currentThread();
    // 从线程中拿到 ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 从 map 中拿到 entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 如果不为空,读取以后 ThreadLocal 中保留的值
        if (e != null) {@SuppressWarnings("unchecked")
            T result = (T) e.value;
            return result;
        }
    }
    // 若 map 为空,则对以后线程的 ThreadLocal 进行初始化,最初返回以后的 ThreadLocal 对象关联的初值,即 value
    return setInitialValue();}

get 办法的次要流程为:

  • 先获取到以后线程的援用
  • 获取以后线程外部的 ThreadLocalMap
  • 如果 map 存在,则获取以后 ThreadLocal 对应的 value 值
  • 如果 map 不存在或者找不到 value 值,则调用 setInitialValue() 进行初始化

get 办法的时序图如下所示:

其中每个 Thread 的 ThreadLocalMap 以 threadLocal 作为 key,保留本人线程的 value 正本,也就是保留在每个线程中,并没有保留在 ThreadLocal 对象中。

其中 ThreadLocalMap.getEntry() 办法的源码如下:

/**
 * 返回 key 关联的键值对实体
 *
 * @param key threadLocal
 * @return
 */
private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    // 若 e 不为空,并且 e 的 ThreadLocal 的内存地址和 key 雷同,间接返回
    if (e != null && e.get() == key) {return e;} else {
        // 从 i 开始向后遍历找到键值对实体
        return getEntryAfterMiss(key, i, e);
    }
}

ThreadLocalMap 的 resize 办法

当 ThreadLocalMap 中的 ThreadLocal 的个数超过容量阈值时,ThreadLocalMap 就要开始扩容了,咱们一起来看下 resize 的源代码:

/**
 * 扩容,从新计算索引,标记垃圾值,不便 GC 回收
 */
private void resize() {Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    // 新建一个数组,依照 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();
            // 若有垃圾值,则标记清理该元素的援用,以便 GC 回收
            if (k == null) {e.value = null;} else {
                // 计算 ThreadLocal 在新数组中的地位
                int h = k.threadLocalHashCode & (newLen - 1);
                // 如果发生冲突,应用线性探测往后寻找适合的地位
                while (newTab[h] != null) {h = nextIndex(h, newLen);
                }
                newTab[h] = e;
                count++;
            }
        }
    }
    // 设置新的扩容阈值,为数组长度的三分之二
    setThreshold(newLen);
    size = count;
    table = newTab;
}

resize 办法次要是进行扩容,同时会将垃圾值标记不便 GC 回收,扩容后数组大小是原来数组的两倍。

ThreadLocal 利用场景

ThreadLocal 的个性也导致了利用场景比拟宽泛,次要的利用场景如下:

  • 线程间数据隔离,各线程的 ThreadLocal 互不影响
  • 不便同一个线程应用某一对象,防止不必要的参数传递
  • 全链路追踪中的 traceId 或者流程引擎中上下文的传递个别采纳 ThreadLocal
  • Spring 事务管理器采纳了 ThreadLocal
  • Spring MVC 的 RequestContextHolder 的实现应用了 ThreadLocal

正文完
 0