深入详解ThreadLocal 您所在的位置:网站首页 threadlocal意义 深入详解ThreadLocal

深入详解ThreadLocal

2024-07-16 21:01| 来源: 网络整理| 查看: 265

本文字数:8349字,阅读大约需要 27 分钟。

大家好,我是 BookSea。

在我们日常的并发编程中,有一种神奇的机制在静悄悄地为我们解决着各种看似棘手的问题,它就是「ThreadLocal」。

这个朴素却强大的工具,许多Java开发者可能并没有真正了解过其内部运作原理和应用场景。

本篇文章,我将和大家一起探索 JDK 中这个独特而又强大的类——ThreadLocal。

透过本文,我们将揭开它神秘的面纱,并深入理解它是如何优雅处理线程级别的数据隔离,以及在实际开发中如何有效地利用它。

话不多说,我们进入正题。

什么是ThreadLocal

ThreadLocal是Java中的一个类,它提供了一种线程绑定机制,可以将状态与线程(Thread)关联起来。每个线程都会有自己独立的一个ThreadLocal变量,因此对该变量的读写操作只会影响到当前执行线程的这个变量,而不会影响到其他线程的同名变量。

我们先来看一个简单的ThreadLocal使用示例:

代码语言:javascript复制public class ThreadLocalTest { private static ThreadLocal threadLocal = new ThreadLocal(); public static void main(String[] args) { Thread thread1 = new Thread(() -> { threadLocal.set("本地变量1"); print("thread1"); System.out.println("线程1的本地变量的值为:"+threadLocal.get()); }); Thread thread2 = new Thread(() -> { threadLocal.set("本地变量2"); print("thread2"); System.out.println("线程2的本地变量的值为:"+threadLocal.get()); }); thread1.start(); thread2.start(); } public static void print(String s){ System.out.println(s+":"+threadLocal.get()); }

执行结果如下:

代码语言:javascript复制thread2:本地变量2 thread1:本地变量1 线程2的本地变量的值为:本地变量2 线程1的本地变量的值为:本地变量1

通过上面的例子,我们可以很轻易的看出,ThreadLocal消除了不同线程间共享变量的需求,可以用来实现「线程局部变量」,从而避免了多线程同步(synchronization)的问题。

OK,下面开始讲解ThreadLocal,讲ThreadLocal之前,我们得先从 Thread 类讲起。

在 Thread 类中有维护两个 ThreadLocal.ThreadLocalMap 对象,分别是:threadLocals 和inheritableThreadLocals。

源码如下:

代码语言:javascript复制/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; /* * InheritableThreadLocal values pertaining to this thread. This map is * maintained by the InheritableThreadLocal class. */ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

初始它们都为 null,只有在调用 ThreadLocal 类的 set 或 get 时才创建它们。ThreadLocalMap可以理解为线程私有的HashMap。

ThreadLoalMap是ThreadLocal中的一个静态内部类,是一个类似HashMap的数据结构,但并没有实现Map接口。

ThreadLoalMap中初始化了一个「大小16的Entry数组」,Entry对象用来保存每一个key-value键值对。key是ThreadLocal对象。

有图有真相,源码中的定义如下:

细心的你肯定发现了,Entry继承了「弱引用(WeakReference)」。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。

ThreadLocal 原理

ThreadLocal中我们最常用的肯定是set()和get()方法了,所以先从这两个方法入手。

set方法

当我们调用 ThreadLocal 的 set() 方法时实际是调用了当前线程的 ThreadLocalMap 的 set() 方法。

ThreadLocal 的 set() 方法中,会进一步调用Thread.currentThread() 获得当前线程对象 ,然后获取到当前线程对象的ThreadLocalMap,判断是不是为空。

为空就先调用creadMap()创建 ThreadLocalMap 对象,在构造参数里set进变量。

不为空就直接set(value) 。

这种保证线程安全的方式有个专业术语,称为「线程封闭」,线程只能看到自己的ThreadLocal变量。线程之间是互相隔离的。

get方法

get()方法用来获取与当前线程关联的ThreadLocal的值。

如果当前线程没有该ThreadLocal的值,则调用「initialValue函数」获取初始值返回,所以一般我们使用时需要继承该函数,给出初始值(不重写的话默认返回Null)。

代码语言:javascript复制/** * Returns the value in the current thread's copy of this * thread-local variable. If the variable has no value for the * current thread, it is first initialized to the value returned * by an invocation of the {@link #initialValue} method. * * @return the current thread's value of this thread-local */ public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }

get方法的流程主要是以下几步:

获取当前的Thread对象,通过getMap获取Thread内的ThreadLocalMap。如果map已经存在,以当前的ThreadLocal为键,获取Entry对象,并从从Entry中取出值。否则,调用setInitialValue进行初始化。

我们可以重写initialValue(),设置初始值,具体写法如下:

代码语言:javascript复制private static final ThreadLocal threadLocal = new ThreadLocal(){ @Override protected Integer initialValue() { return Integer.valueOf(0); } }

推荐设置初始值,如果不设置为null,在某些情况下会引发空指针的问题。

remove方法

最后一个需要探究的就是remove()方法,它用于在map中移除一个不用的Entry。

先计算出hash值,若是第一次没有命中,就循环直到null,在此过程中也会调用「expungeStaleEntry」清除空key节点。代码如下:

代码语言:javascript复制public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } /** * Remove the entry for key. */ private void remove(ThreadLocal key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } }

上面我们看了ThreadLocal的源码,我们知道 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。

出现「内存泄漏」的问题。

其实在执行 ThreadLocal 的 set、remove、rehash 等方法时,它都会扫描 key 为 null 的 Entry,如果发现某个 Entry 的 key 为 null,则代表它所对应的 value 也没有作用了,所以它就会把对应的 value 置为 null,这样,value 对象就可以被正常回收了。

但是假设 ThreadLocal 已经不被使用了,那么实际上 set、remove、rehash 方法也不会被调用,与此同时,如果这个线程又一直存活、不终止的话,那么刚才的那个调用链就一直存在,也就导致了内存泄漏。

ThreadLocal 的Hash算法

ThreadLocalMap类似HashMap,它有自己的Hash算法。

代码语言:javascript复制private final int threadLocalHashCode = nextHashCode(); private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } public final int getAndAdd(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta); }

HASH_INCREMENT这个数字被称为「斐波那契数」 也叫 「黄金分割数」,其中的数学原理我们不去纠结,

我们只需知道用斐波那契数去散列,带来的好处就是 hash分布非常均匀。

每当创建一个ThreadLocal对象,这个ThreadLocal.nextHashCode 这个值就会增长 0x61c88647 。

讲到Hash就会涉及到Hash冲突,跟HashMap通过「链地址法」不同的是,ThreadLocal是通过「线性探测法/开放地址法」来解决hash冲突。

ThreadLocal 1.7和1.8的区别

ThreadLocal 1.7版本的时候,entry对象的key是Thread。到了1.8版本entry的key是ThreadLocal。

1.8版本的好处是当Thread销毁的时候,ThreadLocalMap也会随之销毁,减少内存的使用。因为ThreadLocalMap是Thread的内部类,所以只要Thread消失了,那ThreadLocalMap就不复存在了。

ThreadLocal 的问题ThreadLocal 内存泄露问题

在 ThreadLocalMap 中的 Entry 的 key 是对 ThreadLocal 的 WeakReference 弱引用,而 value 是强引用。

注意构造函数里的第一行代码super(k),这意味着ThreadLocal对象是一个弱引用

代码语言:javascript复制/** * The entries in this hash map extend WeakReference, using * its main ref field as the key (which is always a * ThreadLocal object). Note that null keys (i.e. entry.get() * == null) mean that the key is no longer referenced, so the * entry can be expunged from table. Such entries are referred to * as "stale entries" in the code that follows. */ static class Entry extends WeakReference k = e.get(); if (k == null) { e.value = null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }

简单叙述下源码说了什么:

遍历散列数组,从开始位置(hash得到的位置)向后探测清理过期数据,如果遇到过期数据,则置为null。

如果碰到的是未过期的数据,则将此数据rehash,然后重新在 table 数组中定位。

如果定位的位置已经存在数据,则往后顺延,直到遇到没有数据的位置。

说白了就是:从当前节点开始遍历数组,将key等于null的entry置为null,key不等于null则rehash重新分配位置,若重新分配上的位置有元素则往后顺延。

启发式清理

启发式清理需要接收两个参数:

探测式清理后返回的数字下标。数组总长度。

cleanSomeSlots()源码:

代码语言:javascript复制private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { i = nextIndex(i, len); Entry e = tab[i]; if (e != null && e.get() == null) { n = len; removed = true; i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); return removed; }

根据源码可以看出,启动式清理会从传入的下标 i 处,向后遍历。如果发现过期的Entry则再次触发探测式清理,并重置 n。

这个n是用来控制 do while 循环的跳出条件。如果遍历过程中,连续 m 次没有发现过期的Entry,就可以认为数组中已经没有过期Entry了。

这个 m 的计算是 n >>>= 1 ,可以理解为是数组长度的2的几次幂。

例如:数组长度是16,那么2^4=16,也就是连续4次没有过期Entry。

说白了就是: 从当前节点开始,进行do-while循环检查清理过期key,结束条件是连续n次未发现过期key就跳出循环,n是经过位运算计算得出的,可以简单理解为数组长度的2的多少次幂次。

触发时机

这两种清理方式会在源码中多个位置被触发。

下面的触发场景中,我都从源码中找到了对应的位置,直接对号入座即可,有兴趣的可以去深入阅读这部分的源码。

set() 方法中,遇到key=null的情况会触发一轮探测式清理流程。set() 方法最后会执行一次启发式清理流程。rehash() 方法中会调用一次探测式清理流程。get() 方法中遇到key过期的时候会触发一次探测式清理流程。启发式清理流程中遇到key=null的情况也会触发一次探测式清理流程。

最后,给本篇文章做个总结。

总结

ThreadLocal是Java提供的一种非常有用的工具,它可以帮助我们在每个线程中存储并管理各自独立的数据副本。这种特性使得ThreadLocal在处理多线程编程中的某些问题时极为高效且易于使用,例如实现线程安全、维护线程间的数据隔离等。

然而,ThreadLocal也要谨慎使用,因为不正确的使用可能会导致内存泄漏。特别是在使用完ThreadLocal后,我们需要记住及时调用其remove()方法清理掉线程局部变量,防止对已经不存在的对象的长时间引用,引发内存泄漏。

总的来说,ThreadLocal具有强大的功能,但必须了解其工作原理和可能的风险,才能充分利用它而不会产生意料之外的问题。因此, 深入理解并合理使用ThreadLocal是每个Java开发者的必备技能。

本篇文章到这结束啦~,觉得有收获点个赞哦。

一起交流学习,期待与你共同进步



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有