Java 并发编程:线程变量 ThreadLocal

大家好,我是栗筝i,这篇文章是我的 "栗筝i 的 Java 技术栈" 专栏的第 029 篇文章,在 "栗筝i 的 Java 技术栈" 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验,并希望进一步完善自己对整个 Java 技术体系来充实自己的技术栈的同学。与此同时,本专栏的所有文章,也都会准备充足的代码示例和完善的知识点梳理,因此也十分适合零基础的小白和要准备工作面试的同学学习。当然,我也会在必要的时候进行相关技术深度的技术解读,相信即使是拥有多年 Java 开发经验的从业者和大佬们也会有所收获并找到乐趣。

--

在并发编程中,线程安全性始终是开发者关注的重点。为了避免多个线程对同一共享变量的竞争,通常需要复杂的同步机制。然而,ThreadLocal 提供了一种更为简洁的解决方案,它通过为每个线程提供独立的变量副本,避免了线程间的共享状态,极大简化了并发编程中的数据管理。本文将探讨 ThreadLocal 的使用方法、底层实现原理,以及其在实际开发中的应用场景和潜在的内存泄漏问题。通过对 ThreadLocal 的深入理解,读者将能够更有效地管理线程中的数据,提高并发程序的安全性与性能。


文章目录

      • [1、ThreadLocal 简介](#1、ThreadLocal 简介)
      • [2、ThreadLocal 的使用](#2、ThreadLocal 的使用)
      • [3、ThreadLocal 原理](#3、ThreadLocal 原理)
        • [3.1、ThreadLocal 原理概述](#3.1、ThreadLocal 原理概述)
        • 3.2、ThreadLocalMap
        • [3.3、ThreadLocal 相关源码解析](#3.3、ThreadLocal 相关源码解析)
      • [4、ThreadLocal 内存泄漏问题](#4、ThreadLocal 内存泄漏问题)
        • [4.1、ThreadLocal 内存泄漏问题发生的原因](#4.1、ThreadLocal 内存泄漏问题发生的原因)
        • 4.2、为什么使用弱引用
        • [4.3、ThreadLocal 最佳实践](#4.3、ThreadLocal 最佳实践)
      • [5、Thread 相关知识点](#5、Thread 相关知识点)
        • [5.1、关于 ThreadLocal 和 Synchronized 的区别](#5.1、关于 ThreadLocal 和 Synchronized 的区别)
        • [5.2、关于 ThreadLocalMap 中的 Hash 冲突处理](#5.2、关于 ThreadLocalMap 中的 Hash 冲突处理)

1、ThreadLocal 简介

ThreadLocal 即线程变量,是 Java 提供的用于实现线程本地变量的工具类。每个线程可以通过 ThreadLocal 对象访问其专属的变量,避免了多线程环境下变量共享导致的数据不一致问题。

通常情况下,我们创建的成员变量都是线程不安全的。因为他可能被多个线程同时修改,此变量对于多个线程之间彼此并不独立,是共享变量。而 ThreadLocal 中填充的变量属于当前线程,该变量对其他线程而言是隔离的。

Ps:ThreadLocal 很容易让人望文生义,想当然地认为是一个 "本地线程"。其实,ThreadLocal 并不是一个 Thread,而是 Thread 的局部变量,也许把它命名为 ThreadLocalVariable 更容易让人理解一些。

ThreadLocal 为变量在每个线程中都创建了一个属于当前 Thread 的副本,且该副本只能由当前 Thread 使用,其它 Thread 不可访问,因此也就不存在多线程间共享的问题了。

ThreadLocal 变量通常被 private static 修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

在适用场景上 ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。


2、ThreadLocal 的使用

2.1、创建方式

通过 ThreadLocal 的构造方法创建:

java 复制代码
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

通过带初始值的工厂方法创建:

java 复制代码
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);
2.2、常用方法
  • set(T value) -- 设置线程本地变量的内容。
  • get() -- 获取线程本地变量的内容。
  • remove() -- 移除线程本地变量(值变为 null)。Ps:在线程池的线程复用场景中在线程执行完毕时一定要调用 remove,避免在线程被重新放入线程池中时被本地变量的旧状态仍然被保存。
2.3、Demo

既然 ThreadLocal 的作用是每一个线程创建一个副本,我们使用一个例子来验证一下:

java 复制代码
    public static void main(String[] args) {
        // 新建一个ThreadLocal
        ThreadLocal<String> local = new ThreadLocal<>();
        // 新建一个随机数类
        Random random = new Random();
        // 使用 java8 的 Stream 新建 5 个线程
        IntStream.range(0, 5).forEach(a -> new Thread(() -> {
            // 为每一个线程设置相应的 local 值
            local.set(a + "  " + random.nextInt(10));
            System.out.println("线程和local值分别是  " + local.get());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start());
    }
/*
		线程和local值分别是  0  4
		线程和local值分别是  2  8
		线程和local值分别是  3  9
		线程和local值分别是  1  4
		线程和local值分别是  4  4
*/

从结果我们可以看到,每一个线程都有各自的 local 值,我们设置了一个休眠时间,就是为了另外一个线程也能够及时的读取当前的 local 值。


3、ThreadLocal 原理

3.1、ThreadLocal 原理概述

那么如何究竟是如何实现在每个线程里面保存一份单独的本地变量呢?

实际上线程在 Java 中就是一个 Thread 类的实例对象!而 Thread 的实例对象中实例成员字段的内容肯定是这个对象独有的,所以我们也可以将保存 ThreadLocal 线程本地变量作为一个 Thread 类的成员字段,这个成员字段就是:threadLocals。

java 复制代码
/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

每个 Thread 维护着一个 ThreadLocalMap 的引用,而 ThreadLocalMap 又是 ThreadLocal 的内部类,用 Entry 来进行存储,ThreadLocal 创建的副本是存储在自己的 threadLocals 中的,也就是自己的 ThreadLocalMap:

  1. 每个 Thread 维护着一个 ThreadLocalMap 的引用。

  2. ThreadLocalMap 是 ThreadLocal 的内部类,用 Entry 来进行存储

  3. ThreadLocal 创建的副本是存储在自己的 threadLocals 中的,也就是自己的 ThreadLocalMap。

  4. ThreadLocalMap 的键值为 ThreadLocal 对象,而且可以有多个 threadLocal 变量,因此保存在 map 中

  5. 在进行 get 之前,必须先 set,否则会报空指针异常,当然也可以初始化一个,但是必须重写 initialValue() 方法。

  6. ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。

3.2、ThreadLocalMap

ThreadLocalMap 是归 Thread 类所有的。它的引用在 Thread 类里,这也证实了一个问题:ThreadLocalMap 类内部为什么有 Entry 数组,而不是 Entry 对象?

因为你业务代码能 new 好多个 ThreadLocal 对象,各司其职。但是在一次请求里,也就是一个线程里,ThreadLocalMap 是同一个,而不是多个,不管你 new 几次 ThreadLocal,ThreadLocalMap 在一个线程里就一个,因为再说一次,ThreadLocalMap 的引用是在 Thread 里的,所以它里面的 Entry 数组存放的是一个线程里你 new 出来的多个 ThreadLocal 对象。

java 复制代码
    static class ThreadLocalMap {

        /**
         * 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;
            }
        }

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;
      
      	......
          
     }
3.3、ThreadLocal 相关源码解析

ThreadLocal#set 方法的源码:

java 复制代码
public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程的threadLocals字段
    ThreadLocalMap map = getMap(t);
    // 判断线程的threadLocals是否初始化了
    if (map != null) {
        map.set(this, value);
    } else {
        // 没有则创建一个ThreadLocalMap对象进行初始化
        createMap(t, value);
    }
}

ThreadLocal#createMap 方法的源码:

java 复制代码
void createMap(Thread t, T firstValue) {
	t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocal.ThreadLocalMap#set 方法源码:

java 复制代码
/**
* 往map中设置ThreadLocal的关联关系
* set中没有使用像get方法中的快速选择的方法,因为在set中创建新条目和替换旧条目的内容一样常见,
* 在替换的情况下快速路径通常会失败(对官方注释的翻译)
*/
private void set(ThreadLocal<?> key, Object value) {
    // map中就是使用Entry[]数据保留所有的entry实例
    Entry[] tab = table;
    int len = tab.length;
    // 返回下一个哈希码,哈希码的产生过程与神奇的0x61c88647的数字有关
    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) {
            // 已经存在则替换旧值
            e.value = value;
            return;
        }
        if (k == null) {
            // 在设置期间清理哈希表为空的内容,保持哈希表的性质
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 扩容逻辑
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

ThreadLocal#get 方法的源码:

java 复制代码
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 获取ThreadLocal对应保留在Map中的Entry对象
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            // 获取ThreadLocal对象对应的值
            T result = (T)e.value;
            return result;
        }
    }
    // map还没有初始化时创建map对象,并设置null,同时返回null
    return setInitialValue();
}

ThreadLocal#remove 方法的源码:

java 复制代码
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    // 键在直接移除
    if (m != null) {
        m.remove(this);
    }
}

4、ThreadLocal 内存泄漏问题

4.1、ThreadLocal 内存泄漏问题发生的原因

ThreadLocal 自身并不储存值,而是作为 一个 key 来让线程从 ThreadLocal 获取 value。而 Entry 是中的 key 是弱引用(Entry extends WeakReference<ThreadLocal<?>>),如果一个 ThreadLocal 没有外部强引用来引用它,那么系统 GC 的时候,这个 ThreadLocal 势必会被回收。

并且,作为 ThreadLocalMap 的 key,ThreadLocal 被回收后,ThreadLocalMap 就会存在 key 为 null,但 value 不为 null 的 Entry(其实,在预防 ThreadLocal 内存泄漏问题上,Java 也做了一些努力:Java 在 Thread 中维护了 ThreadLocalMap,所以 ThreadLocalMap 的生命周期和 Thread(当前线程)一样长。并且,在 ThreadLocal 中,进行 get,set 操作的时候会清除 Map 里所有 key 为 null 的 value。)

但是,若当前线程一直不结束,可能是作为线程池中的一员,线程结束后不被销毁,或者分配(当前线程又创建了 ThreadLocal 对象)使用了又不再调用 get/set 方法,就可能引发内存泄漏。

其次,就算线程结束了,操作系统在回收线程或进程的时候不是一定杀死线程或进程的,在繁忙的时候,只会清除线程或进程数据的操作,重复使用线程或进程(线程 id 可能不变导致内存泄漏)。因此,key 弱引用并不是导致内存泄漏的原因,而是因为 ThreadLocalMap 的生命周期与当前线程一样长,并且没有手动删除对应 value。

4.2、为什么使用弱引用

通过对上述问题的分析我们可以发现,ThreadLocal 内存泄漏的一个主要原因就是 Entry 是中的 key 是弱引用,那这就有一个问题值得思考:为什么使用弱引用而不是强引用?

较为官方的说法是:为了应对非常大和长时间的用途,哈希表使用弱引用的 key。

下面我们分两种情况讨论:

  1. key 使用强引用:引用的 ThreadLocal 的对象被回收了,但是 ThreadLocalMap 还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 不会被回收,导致 Entry 内存泄漏。
  2. key 使用弱引用**:**引用的 ThreadLocal 的对象被回收了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 也会被回收。value 在下一次 ThreadLocalMap 调用 set,get,remove 的时候会被清除。

比较两种情况,我们可以发现:

由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用 ThreadLocal 不会内存泄漏,对应的 value 在下一次 ThreadLocalMap 调用 set,get,remove 的时候会被清除。

因此,ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏,而不是因为弱引用。

4.3、ThreadLocal 最佳实践

综合上面的分析,我们可以理解 ThreadLocal 内存泄漏的前因后果,那么怎么避免内存泄漏呢?

每次使用完 ThreadLocal,都调用它的 remove() 方法,清除数据。

在使用线程池的情况下,没有及时清理 ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用 ThreadLocal 就跟加锁完要解锁一样,用完就清理。


5、Thread 相关知识点

5.1、关于 ThreadLocal 和 Synchronized 的区别

相同点:ThreadLocal 和 Synchronized 都是为了解决多线程中访问相同变量的冲突问题。

不同点:

  • ThreadLocal:以空间换时间,为每个线程提供一个变量副本,消耗较多的内存,但是多个线程可以同时访问该变量而且相互不会影响。
  • Synchronized:以时间换空间,多个线程访问的是同一个变量,但是当多个线程同时访问该变量时,需要抢占锁,并且等待获取锁的线程释放锁,会消耗较多的时间。
5.2、关于 ThreadLocalMap 中的 Hash 冲突处理

ThreadLocalMap 作为一个 HashMap 和 java.util.HashMap 的实现是不同的。对于 java.util.HashMap 使用的是链表法来处理冲突。

但是,对于 ThreadLocalMap,它使用的是简单的线性探测法,如果发生了元素冲突,那么就使用下一个槽位存放。具体来说:

  • 当插入一个键值对时,如果当前索引位置已经被占用,则继续探测下一个位置,直到找到一个空闲的位置为止。
  • 这种方法的优点是实现简单,缺点是当发生冲突时可能会导致探测时间较长,特别是在负载因子较高的情况下。

通过对比可以看出,ThreadLocalMap 的线性探测法更适合于线程本地变量的存储,因为在多数情况下,ThreadLocalMap 的负载因子较低,冲突较少,线性探测法的性能影响较小。

相关推荐
Shaoshing2 小时前
ThreadLocal
java·jvm·threadlocal
ly76891 个月前
生产环境 Spring Boot 应用内存泄漏排查实战
java·spring boot·spring·内存泄漏·threadlocal·gc日志·堆转储
~木雨1 个月前
Java 线程池七问七答:参数、执行流程、拒绝策略到 ThreadLocal 内存泄漏,面试必背
java·面试·线程池·threadlocal·threadpool·executor
linmoo19864 个月前
Java踩坑系列之二:ThreadLocal内存泄漏
java·内存泄漏·threadlocal·踩坑
tongluowan0074 个月前
ThreadLocal底层原理 - 大白话+学术版解释
java·多线程·threadlocal
阿维的博客日记5 个月前
ThreadLocal 是干什么的吗?它核心解决什么问题?
threadlocal
阿维的博客日记5 个月前
ThreadLocal里面存了什么东西,那个泛型,就是Thread里面ThreadLocalMap里面ThreadLocal对应的Value了是吗?
threadlocal
最后一支迷迭香5 个月前
Redis+ThreadLocal实现防重复提交,参考美团GTIS防重系统
redis·threadlocal·防止重复提交
lee_curry6 个月前
线程中断,等待,唤醒与ThreadLocal
java·线程·juc·threadlocal·中断
__土块__6 个月前
Java 大厂一面模拟:从线程本地存储到分库分表路由的连环拷问
kafka·线程池·分库分表·java面试·threadlocal·缓存一致性·大厂一面