【底层原理之旅—ThreadLocal深入浅出的源码分析|Java 刷题打卡

简介: 【底层原理之旅—ThreadLocal深入浅出的源码分析|Java 刷题打卡

题目


ThreadLocal深入浅出的源码分析




知识点


基本介绍


  • ThreadLocal是对Thread内部的局部变量ThreadLocalMap的维护类当线程持有多个ThreadLocal的操作时,会在ThreadLocalMap中通过key进行寻找。
  • 每个Thread里面维护了一个ThreadLocal.ThreadLocalMap变量,底层存储结构为Entry[],ThreadLocal实例作为ThreadLocalMap的key,set/get的值为Map的value,其中,key的引用为弱引用
  • 当执行ThreadLocal.set时,实际是将ThreadLocal对象和值通过key-value的形式放进了Thread中的ThreadLocal.ThreadLocalMap属性中,完成了线程隔离存储,保证了线程安全,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。




使用场景


  • ThreadLocal即便你没有直接用到过,它也间接的出现在你使用过的框架里:


  1. Spring的事务管理
  2. Hibernate的会话管理
  3. logback(和log4j)中的MDC功能实现等
  4. 比如用到的一些分页功能的实现往往也会借助于ThreadLocal
  5. 全链路追踪中的traceId或者流程引擎中上下文的传递一般采用ThreadLocal
  6. Spring MVC的RequestContextHolder的实现使用了ThreadLocal






总体概述


ThreadLocal常用来做线程隔离,下面将对ThreadLocal的实现原理、设计理念、内部实现细节(Map、弱引用)、还有ThreadLocal存在的内存泄露问题进行讲解。




作用目的

提供一个线程内公共变量,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度,让线程的本地变量进行隔离




原理概述


内部结构图

image.png

引用逻辑图(虚线表示弱引用)

image.png


原理分析


  • 一个线程内可以存多个ThreadLocal对象,存储的位置位于Thread的ThreadLocal.ThreadLocalMap变量,在Thread中有如下变量:
/* ThreadLocal values pertaining to this thread. 
 * This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
复制代码


ThreadLocalMap是由ThreadLocal维护的静态内部类,正如代码中注解所说这个变量是由ThreadLocal维护的。


我们在使用ThreadLocal的get()、set()方法时,其实都是调用了ThreadLocalMap类对应的get()、set()方法

  • Thread中的这个变量的初始化通常是在首次调用ThreadLocal的get()、set()方法时进行的。
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
复制代码



  • 上述set方法中,首先获取当前线程对象,然后通过getMap方法来获取当前线程中的threadLocals
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
复制代码



  • 如果Thread中的对应属性为null,则创建一个ThreadLocalMap并赋值给Thread:
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
复制代码



  • 如果已经存在,则通过ThreadLocalMap的set方法设置值,这里我们可以看到set中key为this,也就是当前ThreadLocal对象,而value值则是我们要存的值。

对应的get方法源码如下:

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();
}
复制代码


可以看到同样通过当前线程,拿到当前线程的threadLocals属性,然后从中获取存储的值并返回。在get的时候,如果Thread中的threadLocals属性未进行初始化,则也会间接调用createMap方法进行初始化操作。

image.png



数据结构


  • ThreadLoalMap是ThreadLocal中的一个静态内部类,类似HashMap的数据结构,但并没有实现Map接口
  • ThreadLocalMap中初始化了一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对。通过上面的set方法,我们已经知道其中的key永远都是ThreadLocal对象



看一下相关的源码:

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    private static final int INITIAL_CAPACITY = 16;
    // ...
}
复制代码


ThreadLoalMap的类图结构如下:

image.png



这里需要留意的是,ThreadLocalMap类中的Entry对象继承自WeakReference,也就是说它是弱引用。


由于ThreadLocalMaps是延迟创建的,因此在构造时至少要创建一个Entry对象。这里可以从构造方法中看到:

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}
复制代码


上述构造方法,创建了一个默认长度为16的Entry数组,通过hashCode与length位运算确定索引值i。而上面也提到,每个Thread都有一个ThreadLocalMap类型的变量。


至此,结合Thread,我们可以看到整个数据模型如下:


image.png




hash冲突及解决


  • 我们留意到构造方法中Entry在table中存储位置是通过hashcode算法获得。每个ThreadLocal对象都有一个hash值threadLocalHashCode,每初始化一个ThreadLocal对象,hash值就增加一个固定的大小0x61c88647
  • 在向ThreadLocalMap中的Entry数值存储Entry对象时,会根据ThreadLocal对象的hash值,定位到table中的位置i。



这里分三种情况:


  • 如果当前位置为空的,直接将Entry存放在对应位置
  • 如果位置i已经有值且这个Entry对象的key正好是即将设置的key,那么重新设置Entry中的value
  • 如果位置i的Entry对象和即将设置的key没关系,则寻找一个空位置


计算hash值便会有hash冲突出现,常见的解决方法有:再哈希法、开放地址法、建立公共溢出区、链式地址法等



  • 上面的流程可以看出这里采用的是开放地址方法,如果当前位置有值,就继续寻找下一个位置,注意table[len-1]的下一个位置是table[0],就像是一个环形数组,所以也叫闭散列法。
  • 如果一直都找不到空位置就会出现死循环,发生内存溢出。当然有扩容机制,一般不会找不到空位置的。



内存泄露


ThreadLocal使用不当可能会出现内存泄露,进而可能导致内存溢出。下面我们就来分析一下内存泄露的原因及相关设计思想。



内存引用链路


  • 每个Thread维护一个ThreadLocalMap,它key是ThreadLocal实例本身,value是业务需要存储的Object。
  • ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。
  • 仔细观察ThreadLocalMap,这个map是使用ThreadLocal的弱引用作为Key的,弱引用的对象在GC时会被回收。因此使用了ThreadLocal后,引用链如图所示:



泄露原因分析


  • 正常来说,当Thread执行完会被销毁,Thread.threadLocals指向的ThreadLocalMap实例也随之变为垃圾,它里面存放的Entity也会被回收。这种情况是不会发生内存泄漏的
  • 发生内存泄露的场景一般存在于线程池的情况下。此时,Thread生命周期比较长(存在循环使用),threadLocals引用一直存在,当其存放的ThreadLocal被回收(弱引用生命周期比较短)后,对应的Entity就成了key为null的实例,但value值不会被回收
  • 如果此Entity一直不被get()、set()、remove(),就一直不会被回收,也就发生了内存泄漏



所以,通常在使用完ThreadLocal后需要调用remove()方法进行内存的清除。



为什么使用弱引用而不是强引用?


从表面上看内存泄漏的根源在于使用了弱引用,但为什么JDK采用了弱引用的实现而不是强引用呢?

先来看ThreadLocalMap类上的一段注释:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.


为了协助处理数据比较大并且生命周期比较长的场景,hash table的条目使用了WeakReference作为key

这跟我们想象的有些不同,弱引用反而是为了解决内存存储问题而专门使用的。



我们先来假设一下,如果key使用强引用,那么在其他持有ThreadLocal引用的对象都回收了,但ThreadLocalMap依旧持有ThreadLocal的强引用,这就导致ThreadLocal不会被回收,从而导致Entry内存泄露。


  • 对照一下,弱引用的情况。持有ThreadLocal引用的对象都回收了,ThreadLocalMap持有的是ThreadLocal的弱引用,会被自动回收。 (防止用户获取一些不应该获取的数据,因为数据已经被回收了!)
  • 只不过对应的value值,需要在下次调用set/get/remove方法时会被清除。



综合对比会发现,采用弱引用反而多了一层保障,ThreadLocal被清理后key为null,对应的value在下一次ThreadLocalMap调用set、get、remove的时候可能会被清除



所以,内存泄露的根本原因是是否手动清除操作,而不是弱引用。





扩展延伸


一个线程内可以存多个ThreadLocal对象,存储的位置位于Thread的ThreadLocal.ThreadLocalMap变量,在Thread中有如下变量


为什么将Map放在每一个Thread里

应为如果将Map放在ThreadLocal中进行维护,即使使用ConcurrentHashMap减少并发竞争,但在形式上还是存在线程间的竞争,而放在各个线程中独立维护,就十分满足线程隔离的设计理念。




ThreadLocal.ThreadLocalMap 与 HashMap有什么不同
解决hash冲突方法不同



  • HashMap采用的是数组加链表的结构进行存储,当出现hash冲突时,进行链表追加。
  • ThreadLocal.ThreadLocalMap采用的是开放定址法,即寻找下一个没有存储数据的位置。

拓展: 解决hash冲突的方式1. 开放定址法 2. 再hash 3. 链地址法 4. 公共溢出区。



扩容机制不同

当ThreadLocal.ThreadLocalMap的size大于数据1/2时,会扩容2倍。



为什么Entry的key存储采用弱引用

当ThreadLocal没有引用时,ThreadLocal.ThreadLocalMap依旧存在于Thread中,而ThreadLocal对应的Entry永远不会被使用到,所以采用了弱引用,当ThreadLocal没有引用时,自动key就被GC回收



为什么Entry的value存储没有采用弱引用

我们存储的对象除了ThreadLocalMap的Value就没有其他的引用了,value一但是对象的弱引用,GC的时候被回收,对象就无法访问了,这显然不是我们想要的。





如何解决


  • 在ThreadLocal不使用时,调用remove方法,将Entry从Map中移除,即可解决。
  • 对于Java8 ThreadLocalMap 的 set 方法通过调用 replaceStaleEntry 方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏
  • get方法会间接调用expungeStaleEntry 方法将键和值为 null 的 Entry 设置为 null 从而使得该 Entry 可被回收








相关文章
|
6天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
6天前
|
Java
Java之CountDownLatch原理浅析
本文介绍了Java并发工具类`CountDownLatch`的使用方法、原理及其与`Thread.join()`的区别。`CountDownLatch`通过构造函数接收一个整数参数作为计数器,调用`countDown`方法减少计数,`await`方法会阻塞当前线程,直到计数为零。文章还详细解析了其内部机制,包括初始化、`countDown`和`await`方法的工作原理,并给出了一个游戏加载场景的示例代码。
Java之CountDownLatch原理浅析
|
8天前
|
Java 索引 容器
Java ArrayList扩容的原理
Java 的 `ArrayList` 是基于数组实现的动态集合。初始时,`ArrayList` 底层创建一个空数组 `elementData`,并设置 `size` 为 0。当首次添加元素时,会调用 `grow` 方法将数组扩容至默认容量 10。之后每次添加元素时,如果当前数组已满,则会再次调用 `grow` 方法进行扩容。扩容规则为:首次扩容至 10,后续扩容至原数组长度的 1.5 倍或根据实际需求扩容。例如,当需要一次性添加 100 个元素时,会直接扩容至 110 而不是 15。
Java ArrayList扩容的原理
|
14天前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
31 2
|
14天前
|
算法 Java 数据库连接
Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性
本文详细介绍了Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性。连接池通过复用数据库连接,显著提升了应用的性能和稳定性。文章还展示了使用HikariCP连接池的示例代码,帮助读者更好地理解和应用这一技术。
29 1
|
6天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
15天前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
2天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
15 9
|
5天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
2天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin