2023-05-16
源码分析
0

目录

ThreadLocal
什么是ThreadLocal
ThreadLocal的使用
ThreadLocal的源码分析
“强软弱虚”引用
什么是一个引用
强引用(Strongly Reference)
软引用(Soft Reference)
弱引用(Weak Reference)
虚引用(Phantom Reference)
弱引用的问题
总结

《JUC入门的知识体系梳理》中聊过volatile保证了变量的可见性与有序性,但是不保证变量的原子性,所以在并发下,volatile并不能保证变量的安全性,那怎么保证线程中变量的安全性呢?今天我们来聊下ThreadLocal,从源码的角度分析,看一看它是如何保证线程之间的变量安全的。

ThreadLocal

什么是ThreadLocal

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

在JDK1.2中引入了ThreadLocal类,来修饰共享变量,使每个线程都单独拥有一份共享变量,这样就可以做到线程之间对于共享变量的隔离问题

此时回想一下JMM内存模型,再想一下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中存在一个ThreadLocalMap属性,ThreadLocalMap 本身就是基于 Entry[] 实现存储数据的,其中 ThreadLocal 作为key,ThreadLocal的值作为value,这种数据结构就是一个线程可以绑定多个ThreadLocal的原因

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

那么什么是引用呢?接下来我们聊下什么是引用,然后再来继续分析源码

“强软弱虚”引用

看过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被回收了,那么ThreadLocalMap中的Entry的key就为null,那么value就无法获取了,但此时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 是什么以及使用,通过源码分析了它是怎么做到线程的隔离,本质就是每个Thread中都有一份 ThreadLocal.ThreadLocalMap 的属性,key为ThreadLocal,value是变量值,通过ThreadLocal #get() 方法,拿 ThreadLocal 当前的this对象从Thread中获取值。

接着从源码观察到 ThreadLocal 的内部类 ThreadLocalMap 中,它的实现子类Entry继承了弱引用WeakReference,根据弱引用的特点,分析其存在的内存泄漏的风险。又通过源码中remove方法了解到它是怎么解决内存泄漏的问题得。

在我们平时 ThreadLocal 的使用中,做到了知其然,知其所以然,才能更好的使用它。

本文作者:柳始恭

本文链接:

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