2023-05-16
虚拟机与并发
0

目录

ThreadLocal
ThreadLocal的使用
ThreadLocal的源码分析
哈希?哈希!
哈希冲突的解决机制
“强软弱虚”引用
什么是一个引用
引用的类型
强引用(Strongly Reference)
软引用(Soft Reference)
弱引用(Weak Reference)
虚引用(Phantom Reference)
弱引用的问题
ThreadLocal扩展之InheritableThreadLocal
ThreadLocal优化之FastThreadLocal
使用场景
注意事项
总结

在并发编程中,多个线程同时访问和修改共享变量是一个常见的场景。这种情况下,可能会出现线程安全问题,即多个线程对共享变量的操作可能会相互干扰,导致数据不一致。为了解决线程安全问题,ThreadLocal 应然而生,它与 volatile 很像,但是又不完全一致,接下来我们来分析分析。

ThreadLocal

ThreadLocal 意为线程本地变量,用于解决多线程并发时访问共享变量的问题。

ThreadLocal 提供了一种空间换时间的方式来解决线程安全问题。它为每个线程创建了一个独立的存储空间,用于保存线程特有的数据。当多个线程访问同一个 ThreadLocal 变量时,实际上它们访问的是各自线程本地存储的副本,而不是共享变量本身。因此,每个线程都可以独立地修改自己的副本,而不会影响到其他线程。

使用 ThreadLocal 的好处在于它避免了线程之间的竞争和阻塞,提高了并发性能。同时,它也简化了编程模型,因为开发者不需要显式地使用锁来保护共享变量的访问。此外,在使用 ThreadLocal 时也需要注意内存泄漏和数据污染的问题,需要正确地管理和清理线程本地存储的数据。

此时回想 volatile,是不是存在一定的相似性呢?

ThreadLocal 主要适用于每个线程需要独立保存自己的数据副本的情况。volatile 适用于多个线程之间需要共享数据并进行协作,是最轻量级的同步机制。

volatile 它的实现原理是将变量的值刷新到主内存中,每次读取都从主内存中读取,而不是从线程的工作内存中读取。而ThreadLocal是将主内存的变量copy一份到线程的工作内存中,每次操作的都是线程内工作内存中的变量,天然的隔离性,不需要加锁,也不需要刷新到主内存中,保证了变量的安全性,那么它究竟是如何实现的呢?待会源码分析中揭晓。

ThreadLocal的使用

ThreadLocal的使用非常简单,只需要创建一个ThreadLocal对象,调用set()、get()、remove()方法即可。

java
ThreadLocal<String> threadLocal = new ThreadLocal<>(); threadLocal.set("hello"); threadLocal.get(); threadLocal.remove();

ThreadLocal创建对象的方式有2种:

  • 第一种是直接new一个ThreadLocal对象
java
ThreadLocal<String> threadLocal = new ThreadLocal<>();
  • 第二种是使用ThreadLocal的静态方法withInitial(),传入一个Supplier接口的实现类,在没有调用set()方法的情况下,ThreadLocal的初始值就是Supplier #get()方法返回的值
java
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "init");

ThreadLocal在我们平时的开发中使用的场景并不是太多,大多常用在组件开发上,许多源码中也包含了ThreadLocal,比如Mybatis中的SqlSession,Spring中的TransactionSynchronizationManager等等。

举个实际使用的栗子:

java
public class Demo { /** * ThreadLocal 线程变量,每个线程都有一个副本,互不干扰 */ public final static ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>(); /** * 设置值 * @param value 值 */ public static void set(String value) { THREAD_LOCAL.set(value); } /** * 测试new Thread()与main线程的ThreadLocal变量值 */ public static void main(String[] args) { String main = "Hello World!"; new Thread(() -> { THREAD_LOCAL.set(main); System.out.println(Thread.currentThread().getName() + ":" + THREAD_LOCAL.get()); THREAD_LOCAL.remove(); }).start(); System.out.println("main:" + THREAD_LOCAL.get()); } } // 结果 main:null Thread-0:Hello World!

ThreadLocal的源码分析

ThreadLocal 是一个泛型类,泛型的类型即为存储的数据类型,从它的三个方法get()、set()、remove()开始分析

java
public class ThreadLocal<T> { ... ThreadLocal.ThreadLocalMap threadLocals = null; public T get() { // 获取当前线程 - 联想到个线程都单独拥有一份共享变量,是不是通过当前线程来处理的呢?继续往下分析 Thread t = Thread.currentThread(); // 通过getMap()方法可以看出从当前线程中获取的ThreadLocalMap,此时猜测肯定这个对象存储了当前线程的共享变量 ThreadLocalMap map = getMap(t); // map不为null,说明当前线程的ThreadLocalMap不为null if (map != null) { // 通过ThreadLocalMap的getEntry()方法获取当前ThreadLocal的Entry ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { // Entry 存储了变量的值,此处返回 @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } // 设置初始化的值,猜测与ThreadLocal.withInitial()方法有关 return setInitialValue(); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } public void set(T value) { // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程的ThreadLocalMap ThreadLocalMap map = getMap(t); // 如果ThreadLocalMap不为null,说明当前线程的ThreadLocalMap不为null if (map != null) // 将当前ThreadLocal作为key,value作为value存储到ThreadLocalMap中 map.set(this, value); else // 如果ThreadLocalMap为null,说明当前线程的ThreadLocalMap为null,继续往下分析 createMap(t, value); } void createMap(Thread t, T firstValue) { // 创建一个ThreadLocalMap,绑定到当前线程 t.threadLocals = new ThreadLocalMap(this, firstValue); } ... }

从上面的get/set源码中可以看出,当前的Thread中有一个ThreadLocalMap类型的成员变量threadLocals,在set的时候,如果有数据,用当前线程的ThreadLocalMap存储,如果没有数据,会指向一个新的new ThreadLocalMap()对象。

猜测此当前线程中的ThreadLocalMap变量就是实现线程隔离的关键,set的时候,实际的变量数据是添加到了当前的线程类中的ThreadLocalMap中存储,以下是Thread中的源码

java
public class Thread implements Runnable { ... ThreadLocal.ThreadLocalMap threadLocals = null; ... }

咦,在Thread源码中发现ThreadLocalMap居然是ThreadLocal的内部类,从之前的分析上,看名知意,这应该是个Map结构,那么ThreadLocalMap中存储结构是怎样呢?继续看源码

java
static class ThreadLocalMap { ... // Entry 继承了弱引用 WeakReference,key 即为 ThreadLocal,value 为存储的数据 static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; // key 即为 ThreadLocal,value 为存储的数据 Entry(ThreadLocal<?> k, Object v) { // 指定k变量也就是ThreadLocal为弱引用 super(k); value = v; } ... } // Entry数组,用于存储数据 private Entry[] table; private Entry getEntry(ThreadLocal<?> key) { // 计算key的hash值 int i = key.threadLocalHashCode & (table.length - 1); // 通过hash计算的值定位取出数组中的Entry Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); } ... }

以上源码可以观察到ThreadLocalMap类中存储的是一个Entry数组,Entry是ThreadLocal的内部类,Entry继承了弱引用 WeakReference,key即为 ThreadLocal,value为线程存储的数据

为什么一个线程可以绑定多个ThreadLocal呢?

其实在每个 Thread 线程中都存在一个 ThreadLocal.ThreadLocalMap 属性,ThreadLocalMap 底层结构是基于 Entry[] 实现数据存储的,其中 ThreadLocal 对象作为key,ThreadLocal 的值作为value,这也是一个线程可以绑定多个 ThreadLocal 的原因

回想到set方法,直接是将值set到了当前 Thread 中的 ThreadLocalMap 中,线程与线程之间的变量都存储在自己当前线程中,保证了并发下变量的安全性,这也是隔离性实现的原理

哈希?哈希!

其实ThreadLocal 的get/set 源码还是比较简单的,接下来讲个好玩的,面试说出这个,就说明你是真的看过源码,理解到位了。

我们来看下面的方法,这是 getEntity 方法中根据哈希计算获取数组中的值,如果匹配不上,则调用的方法,其代码核心逻辑是 ThreadLocal 不匹配,则查找下一个索引值,while 判断只要不为null

java
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; } private void set(ThreadLocal<?> key, Object value) { 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)]) { ThreadLocal<?> k = e.get(); if (k == key) { // 找到相同key e.value = value; return; } if (k == null) { // 发现过期条目 replaceStaleEntry(key, value, i); return; } } // 找到空槽 tab[i] = new Entry(key, value); // ... 检查扩容 }

看到此处大家有没有想过哈希冲突的问题,此处是如何解决哈希冲突的?

哈希冲突的解决机制

ThreadLocalMap 是 ThreadLocal 的内部类,它使用了一种特殊的开放地址法(Open Addressing)来解决哈希冲突,具体实现与常规的 HashMap 有所不同,它是采用 线性探测法(Linear Probing) 来处理哈希冲突的。

  • 当计算出的索引位置已被占用时,会顺序查找下一个空槽(index + 1)
  • 如果到达数组末尾,则从数组开头继续查找(环形查找)

这种探测方式比链地址法(如HashMap的链表/红黑树)更节省内存

特殊设计的关键点

  1. 初始容量:默认初始大小为16,必须是2的幂次方
  2. 哈希计算:
java
int i = key.threadLocalHashCode & (table.length - 1);
threadLocalHashCode是一个原子递增的静态变量,每次创建ThreadLocal实例时增加0x61c88647(黄金分割数)

3. 惰性删除:

在探测过程中遇到过期条目(key为null的Entry)会触发清理 这种清理是渐进式的,不是每次操作都完整清理

扩容机制

  • 当元素数量达到阈值(当前容量的2/3)时:
  • 先进行全量过期条目清理
  • 如果清理后仍超过阈值(容量的1/2),则扩容为原来的2倍
  • 重新哈希所有有效条目

为什么此处不直接使用 hashmap?

本身 ThreadLocal 副本的量没有那么大,每个线程独立的、相对少量的变量存储,避免了链表或树结构的额外内存开销,同时通过线性探测和惰性清理保持了较好的性能。

了解完哈希,接下来我们聊下什么是引用,继续分析源码

“强软弱虚”引用

看过JVM虚拟机的小伙伴都了解,在垃圾回收机制中,判断对象是否被回收的标准就是是判断对象是否引用链可达,而判断标准主要分为4种引用,分别是 强、软、弱、虚

什么是一个引用

Object o = new Object() 这就是一个引用了,一个变量指向new出来的对象,这就叫以个引用。当我们new出来一个象,在java语言里是不需要手动回收的,C和C++是需要的,在这种情况下,java的垃圾回收机制会自动的帮你回收这个对象。

再开始之前我们需要重写了一个方法叫 finalize(),在垃圾回收的过程中,各种引用它不同的表现,垃圾回收的时候,它是会调用finalize(),通过重写这个方法之后我们能观察到,它什么时候被垃圾回收了,什么时候被调用了。

java
public class M { @Override protected void finalize() throws Throwable { System.out.println("finalize"); } }

平常的工作中需要重写 finalize() 吗

这个方法为了让我们去观察结果用的,并不是说以后在什么情况下需要重写这个方法。可以在以后面试时遇到相关问题,可以进行阐述,让面试官了解到你造火箭的能力。

引用的类型

强引用(Strongly Reference)

是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象

软引用(Soft Reference)

软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。 在JDK 1.2版之后提供了SoftReference类来实现软引用。

弱引用(Weak Reference)

弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。 在JDK 1.2版之后提供了WeakReference类来实现弱引用

虚引用(Phantom Reference)

虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK1.2版之后提供了PhantomReference类来实现虚引用

弱引用的问题

强软弱虚引用我们已经了解清楚了,那此时回想一下,弱引用的特点是什么?结合我们的 ThreadLocal 源码,如果此时 ThreadLocalMap中的 key -> 也就是ThreadLocal对象被回收了,会怎么样?

image.png

此处就会引发了一个致命问题,如果弱引用的 ThreadLocal 对象,也就是 Entry key 被回收了,那么 ThreadLocalMap 对应的 Entry value 就无法获取了,但此时 Entry 对象还是存在 ThreadLocalMap Entry[] 数组中,这样就导致了内存泄漏

所以ThreadLocalMap中的Entry需要被回收,那么ThreadLocalMap中的Entry是如何被回收的呢?继续看源码

java
// ThreadLocal #remove() public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } // ThreadLocalMap #remove() 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) { // 调用 Reference #clear() 方法,清除弱引用 e.clear(); // 清除数组中的Entry expungeStaleEntry(i); return; } } } // ThreadLocalMap #expungeStaleEntry() private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // 将Entry中的value值设置为null,将Entry对象设为null,方便GC回收 // 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; }

此时分析remove方法,从源码中可以看出,remove方法是通过当前线程获取ThreadLocalMap,然后调用ThreadLocalMap的remove方法

ThreadLocalMap #remove方法是通过key的hash值定位到数组中的Entry,调用 Reference #clear 清除引用

然后调用expungeStaleEntry方法,将Entry中的value值设置为null,将Entry对象设为null,方便GC回收

此时就完美解决了ThreadLocal内存泄漏的问题,这也是为什么我们在使用了ThreadLocal之后,必须调用remove方法的原因,属于强制性的操作,可千万别埋坑啊。

对于其他源码中的方法,就不一一分析了,有兴趣的小伙伴可以自行研究,比如hash计算的方法等,这里只是分析了ThreadLocal实现原理,以及存在的内存泄漏问题、解决方案的核心源码。

ThreadLocal扩展之InheritableThreadLocal

InheritableThreadLocal 是 ThreadLocal 的子类,允许子线程在创建时继承父线程的变量值。它与普通 ThreadLocal 的主要区别在于值继承机制:

  • 值继承‌:子线程创建时会自动继承父线程的 InheritableThreadLocal 值,但后续修改父线程的值不会影响子线程。
  • 适用场景‌:适用于需要线程间传递上下文信息(如日志上下文)的场景,但不适用于线程池(因线程复用问题)。
  • ‌继承时机‌:仅在子线程创建时继承父线程的值,后续父线程修改值不会同步到子线程。 ‌
  • 局限性‌:无法通过其他方式(如显式调用)让子线程获取父线程的更新值。 ‌

这个特性在某些场景下非常有用,比如当你希望在整个线程树中共享某些数据时,但又不希望这些数据被其他无关的线程所访问。然而,需要谨慎使用,因为不正确的使用可能会导致数据污染和内存泄漏。

java
public class InheritableThreadLocalExample { // 创建一个 InheritableThreadLocal 变量 private static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>(); public static void main(String[] args) { // 在主线程中设置值 inheritableThreadLocal.set("这是父线程的值"); System.out.println("父线程中的值: " + inheritableThreadLocal.get()); // 创建一个子线程 Thread childThread = new Thread(() -> { // 在子线程中尝试获取值,由于使用了 InheritableThreadLocal,这里会获取到父线程中设置的值 System.out.println("子线程中的值: " + inheritableThreadLocal.get()); }); // 启动子线程 childThread.start(); // 等待子线程执行完成 try { childThread.join(); } catch (InterruptedException e) { e.printStackTrace(); } // 主线程结束时清除值,防止潜在的内存泄漏 inheritableThreadLocal.remove(); } }

ThreadLocal优化之FastThreadLocal

FastThreadLocal 是 Netty 框架提供的一个高性能的线程局部变量实现,它旨在提供比 Java 标准库中的 ThreadLocal 更快的访问速度。

FastThreadLocal 通过使用内部数组和变量索引技术减少了访问线程局部变量的时间,提高了性能。这种实现特别适合在高频率访问线程局部变量的场景中使用。

FastThreadLocal 的主要优势在于其高效的内存访问模式和减少了间接引用,这有助于减少缓存未命中的情况,并提高内存访问的局部性。然而,需要注意的是,FastThreadLocal 主要是为 Netty 内部使用而设计的,但也可以在普通的 Java 应用中使用,尽管可能需要额外的设置。

java
import io.netty.util.concurrent.FastThreadLocal; import io.netty.util.concurrent.FastThreadLocalThread; public class FastThreadLocalExample { // 创建一个 FastThreadLocal 变量 private static final FastThreadLocal<String> fastThreadLocal = new FastThreadLocal<>(); public static void main(String[] args) { // 由于 FastThreadLocal 是为 Netty 设计的,它通常与 Netty 的线程模型一起使用。 // 但为了演示,我们来创建一个 FastThreadLocalThread 来模拟 Netty 的线程。 FastThreadLocalThread thread = new FastThreadLocalThread(() -> { // 在线程中设置值 fastThreadLocal.set("FastThreadLocal 中的值"); // 获取并打印值 System.out.println("线程中的值: " + fastThreadLocal.get()); // 清除值,防止内存泄漏 fastThreadLocal.remove(); }); // 启动线程 thread.start(); } }

使用场景

  • 数据库连接:在多线程应用中,每个线程可能需要自己的数据库连接。使用 ThreadLocal 可以为每个线程保存其自己的连接。
  • 会话管理:在 Web 应用中,每个用户的会话数据可以使用 ThreadLocal 存储,从而确保同一用户的多个请求在同一个线程中处理时能够访问到正确的会话数据。
  • 线程内上下文传递:有时需要在同一个线程的不同方法之间传递一些上下文信息,而不希望使用全局变量或参数传递。这时可以使用 ThreadLocal。

注意事项

  • 内存泄漏:如果线程不再需要使用该变量,但忘记调用 remove() 方法来清理,那么由于 ThreadLocalMap 中的 Entry 的 key 是对 Thread 的弱引用,所以 Thread 被回收后,Entry 的 key 会被置为 null,但 Entry value 还被关联在 ThreadLocalMap 的 Entry[] 数组中不会被回收,从而导致内存泄漏。因此,使用完 ThreadLocal 后,需要调用 remove() 方法来清理。
  • 线程池中的使用:在线程池中,线程可能会被复用。如果线程之前设置过 ThreadLocal 变量,但在使用后没有清理,那么下一个任务可能会读取到上一个任务设置的值。因此,在线程池中使用 ThreadLocal 时需要特别小心。
  • 初始化问题:如果不重写 initialValue() 方法,并且在使用前没有调用 set() 方法设置值,那么 get() 方法将返回 null。为了避免这种情况,可以重写 initialValue() 方法来提供一个默认值。
  • 不适用于全局共享状态:虽然 ThreadLocal 可以在多个线程之间隔离数据,但它不适用于需要在多个线程之间共享和修改的全局状态。对于这种情况,应该使用其他同步机制(如锁或原子变量)。

总结

总结一下,ThreadLocal是Java并发编程中非常重要的一个类,它提供了线程局部变量的功能,使得每个线程都可以拥有自己独立的变量副本。通过深入了解ThreadLocal的工作原理和用法,我们可以更好地应用它来解决并发编程中的问题。同时,也需要注意ThreadLocal的内存泄漏问题,并采取相应的措施来避免这个问题的发生。在我们平时 ThreadLocal 的使用中,做到了知其然,知其所以然,才能更好的使用它。

本文作者:柳始恭

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!