2023-05-16
源码分析
0

目录

ThreadLocal
什么是ThreadLocal
ThreadLocal的使用
ThreadLocal的源码分析
总结

《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中,线程与线程之间的变量都存储在自己当前线程中,保证了并发下变量的安全性,这也是隔离性实现的原理

此时回想一下,弱引用的特点是什么?如果ThreadLocal对象被回收了,会怎么样?

此处就会引发了一个致命问题,如果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 许可协议。转载请注明出处!