本文已收录至GitHub,举荐浏览 Java随想录
微信公众号:Java随想录
原创不易,重视版权。转载请注明原作者和原文链接
在咱们日常的并发编程中,有一种神奇的机制在静悄悄地为咱们解决着各种看似辣手的问题,它就是「ThreadLocal」。
这个奢侈却弱小的工具,许多Java开发者可能并没有真正理解过其外部运作原理和利用场景。
本篇文章,我将和大家一起摸索 JDK 中这个独特而又弱小的类——ThreadLocal。
透过本文,咱们将揭开它神秘的面纱,并深刻了解它是如何优雅解决线程级别的数据隔离,以及在理论开发中如何无效地利用它。
话不多说,咱们进入正题。
什么是ThreadLocal
ThreadLocal是Java中的一个类,它提供了一种线程绑定机制,能够将状态与线程(Thread)关联起来。每个线程都会有本人独立的一个ThreadLocal变量,因而对该变量的读写操作只会影响到以后执行线程的这个变量,而不会影响到其余线程的同名变量。
咱们先来看一个简略的ThreadLocal应用示例:
public class ThreadLocalTest { private static ThreadLocal<String> 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()); }
执行后果如下:
thread2:本地变量2thread1:本地变量1线程2的本地变量的值为:本地变量2线程1的本地变量的值为:本地变量1
通过下面的例子,咱们能够很轻易的看出,ThreadLocal打消了不同线程间共享变量的需要,能够用来实现「线程局部变量」,从而防止了多线程同步(synchronization)的问题。
OK,上面开始解说ThreadLocal,讲ThreadLocal之前,咱们得先从 Thread
类讲起。
在 Thread
类中有保护两个 ThreadLocal.ThreadLocalMap
对象,别离是:threadLocals
和inheritableThreadLocals
。
源码如下:
/* 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)。
/** * 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()
,设置初始值,具体写法如下:
private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){ @Override protected Integer initialValue() { return Integer.valueOf(0); }}
举荐设置初始值,如果不设置为null,在某些状况下会引发空指针的问题。
remove办法
最初一个须要探索的就是remove()
办法,它用于在map中移除一个不必的Entry。
先计算出hash值,若是第一次没有命中,就循环直到null,在此过程中也会调用「expungeStaleEntry」革除空key节点。代码如下:
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算法。
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对象是一个弱援用
/** * 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<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; }}
其实要解决也简略,只须要应用完 ThreadLocal 后手动调用 remove()
办法。
但其实在 ThreadLocalMap 的实现中以及思考到这种状况,因而在调用 set()
、get()
、remove()
办法时,也会清理 key 为 null 的记录。
为什么应用弱援用而不是强援用?
为什么采纳了弱援用的实现而不是强援用呢?
这个问题在源码的正文上有阐明,咱们来瞅瞅。
原谅我英语不好,这段话应用的某道翻译,翻译如下:
为了帮助解决数据比拟大并且生命周期比拟长的场景,hash table的entry应用了WeakReference作为key。
所以,貌似看起来弱援用反而是为了解决内存存储问题而专门应用的?
认真思考一下,实际上,采纳弱援用反而多了一层保障。
如果ThreadLocal作为key应用强援用,那么只有ThreadLocal实例自身在内存中,它的entry(包含value)就会始终存在于ThreadLocalMap中,即便线程曾经不再须要它的ThreadLocal变量。它们也不会被回收,这导致了一种模式的内存透露。
而如果咱们应用WeakReference作为key。这意味着当对ThreadLocal实例的所有强援用都被垃圾收集器革除后,它的entry(包含value)也能够从ThreadLocalMap中革除,避免了潜在的内存透露。
这样设计让ThreadLocal生命周期的控制权交给了用户,用户能够抉择什么时候完结ThreadLocal实例的生命周期。
ThreadLocal被清理后key为null,对应的value在下一次ThreadLocalMap调用set、get时革除,这样就算遗记调用 remove 办法,弱援用比强援用能够多一层保障。
所以,内存泄露的根本原因在于是否手动革除操作,而不是弱援用。
ThreadLocal 父子线程继承
/* 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;
Thread类里一共有两个ThreadLocal变量,一个是threadLocals,也就是咱们常说的threadlocal,还有一个是inheritableThreadLocals,这个是干啥用的呢?
inheritableThreadLocals是用于父子线程之间继承的,这个变量用来存储那些须要被子线程继承的变量。
并且子线程能够拜访和批改这个变量的值,而不会影响到父线程对应变量的值。
异步场景下无奈给子线程共享父线程的线程正本数据,能够通过 「InheritableThreadLocal」类解决这个问题。
它的原理就是当新建一个线程对象时,子线程是通过在父线程中调用 new Thread()
创立的,在 Thread 的构造方法中调用了 Thread的init()
办法。
在 init()
办法中父线程的「inheritableThreadLocals」数据会复制到子线程:
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals)
上面是一个简略的示例代码:
public class Main { public static void main(String[] args) { // 在主线程中设置 InheritableThreadLocal InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>(); inheritableThreadLocal.set("Hello from the main thread"); // 创立新的线程 Thread thread = new Thread(() -> { // 子线程尝试获取 InheritableThreadLocal 的值 String value = inheritableThreadLocal.get(); System.out.println("In child thread, value from InheritableThreadLocal: " + value); }); thread.start(); // 启动子线程 try { thread.join(); // 期待子线程完结 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("In main thread, value from InheritableThreadLocal: " + inheritableThreadLocal.get()); }}
以上代码首先在主线程中创立了一个 InheritableThreadLocal
对象,并设置了其值。而后,该代码创立并启动了一个新的线程,在新线程中尝试读取 InheritableThreadLocal
的值。因为 InheritableThreadLocal
的值是从父线程继承的,所以新线程可能读取到在主线程中设置的值。
输入如下:
In child thread, value from InheritableThreadLocal: Hello from the main threadIn main thread, value from InheritableThreadLocal: Hello from the main thread
请留神,这种继承是一次性的,只在创立新线程的那一刻产生,之后父子线程对 InheritableThreadLocal
的批改就互不影响了。
同时,因为应用的是浅拷贝,所以如果 InheritableThreadLocal
的值是可变对象,那么仍然可能存在多个线程共享数据的状况。
然而咱们做异步解决个别应用线程池,线程池会复用线程,所以InheritableThreadLocal
在线程池场景中会生效。
不过网上有开源框架,咱们能够应用阿里巴巴的TTL解决这个问题:https://github.com/alibaba/transmittable-thread-local。
探测式清理 & 启发式清理
能把下面那些常识消化完,足够应酬90%的面试和工作场景了。
然而也只能让面试官感觉你有点货色,但不是很多。如果想让面试官直呼牛B,那咱就得来聊聊「探测式清理」和「 启发式清理」了。
ThreadLocal 应用了两种清理有效条目(即键为 null 的条目)的形式:探测式清理和启发式清理。
- 探测式清理(源码中:expungeStaleEntry() 办法 )
- 启发式清理(源码中:cleanSomeSlots() 办法 )
探测式清理
这种清理办法基于一个事实:在查找特定键时,如果遇到有效条目(即键为null的条目),能够平安地删除它,因为它必定不是正在寻找的键。
以下是expungeStaleEntry()
办法的源码:
private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot tab[staleSlot].value = null; tab[staleSlot] = null; size--; // Rehash until we encounter null Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> 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()
源码:
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开发者的必备技能。
本篇文章到这完结了~,那就下次再见吧,感觉有播种点个赞哦。
感激浏览,如果本篇文章有任何谬误和倡议,欢送给我留言斧正。
老铁们,关注我的微信公众号「Java 随想录」,专一分享Java技术干货,文章继续更新,能够关注公众号第一工夫浏览。
一起交流学习,期待与你共同进步!