我把 ThreadLocal 能问的,都写了(上)

简介: 我把 ThreadLocal 能问的,都写了(上)

你好,我是yes。

今天我们再来盘一盘 ThreadLocal ,这篇力求对 ThreadLocal 一网打尽,彻底弄懂 ThreadLocal 的机制。

有了这篇基础之后,下篇再来盘一盘 ThreadLocal 的进阶版,等我哈。

话不多说,本文要解决的问题如下:

  1. 为什么需要 ThreadLocal
  2. 应该如何设计 ThreadLocal
  3. 从源码看ThreadLocal 的原理
  4. ThreadLocal 内存泄露之为什么要用弱引用
  5. ThreadLocal 的最佳实践
  6. InheritableThreadLocal

好了,开车!


为什么需要 ThreadLocal


最近不是开放三胎政策嘛,假设你有三个孩子。

现在你带着三个孩子出去逛街,路过了玩具店,三个孩子都看中了一款变形金刚。

所以你买了一个变形金刚,打算让三个孩子轮着玩。

回到家你发现,孩子因为这个玩具吵架了,三个都争着要玩,谁也不让着谁。

这时候怎么办呢?你可以去拉架,去讲道理,说服孩子轮流玩,但这很累。

所以一个简单的办法就是出去再买两个变形金刚,这样三个孩子都有各自的变形金刚,世界就暂时得到了安宁。

映射到我们今天的主题,变形金刚就是共享变量,孩子就是程序运行的线程。有多个线程(孩子),争抢同一个共享变量(玩具),就会产生冲突,而程序的解决办法是加锁(父母说服,讲道理,轮流玩),但加锁就意味着性能的消耗(父母比较累)。

所以有一种解决办法就是避免共享(让每个孩子都各自拥有一个变形金刚),这样线程之间就不需要竞争共享变量(孩子之间就不会争抢)。

所以为什么需要 ThreadLocal?

就是为了通过本地化资源来避免共享,避免了多线程竞争导致的锁等消耗。

这里需要强调一下,不是说任何东西都能直接通过避免共享来解决,因为有些时候就必须共享。

举个例子:当利用多线程同时累加一个变量的时候,此时就必须共享,因为一个线程的对变量的修改需要影响要另个线程,不然累加的结果就不对了。

再举个不需要共享的例子:比如现在每个线程需要判断当前请求的用户来进行权限判断,那这个用户信息其实就不需要共享,因为每个线程只需要管自己当前执行操作的用户信息,跟别的用户不需要有交集。

好了,道理很简单,这下子想必你已经清晰了 ThreadLocal 出现的缘由了。

再来看一下 ThreadLocal 使用的小 demo。

public class YesThreadLocal {
    private static final ThreadLocal<String> threadLocalName = ThreadLocal.withInitial(() -> Thread.currentThread().getName());
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                System.out.println("threadName: " + threadLocalName.get());
            }, "yes-thread-" + i).start();
        }
    }
}

输出结果如下:


image.png


可以看到,我在 new 线程的时候,设置了每个线程名,每个线程都操作同一个 ThreadLocal 对象的 get 却返回的各自的线程名,是不是很神奇?


应该如何设计 ThreadLocal ?


那应该怎么设计 ThreadLocal 来实现以上的操作,即本地化资源呢?

我们的目标已经明确了,就是用 ThreadLocal 变量来实现线程隔离。

从代码上看,可能最直接的实现方法就是将 ThreadLocal 看做一个 map ,然后每个线程是  key,这样每个线程去调用 ThreadLocal.get 的时候,将自身作为 key 去 map 找,这样就能获取各自的值了!

听起来很完美?错了!

这样 ThreadLocal 就变成共享变量了,多个线程竞争 ThreadLocal ,那就得保证 ThreadLocal 的并发安全,那就得加锁了,这样绕了一圈就又回去了!

所以这个方案不行,那应该怎么做?

答案其实上面已经讲了,是需要在每个线程的本地都存一份值,说白了就是每个线程需要有个变量,来存储这些需要本地化资源的值,并且值有可能有多个,所以怎么弄呢?

在线程对象内部搞个 map,把 ThreadLocal 对象自身作为 key,把它的值作为 map 的值

这样每个线程可以利用同一个对象作为 key ,去各自的 map 中找到对应的值。

这不就完美了嘛!比如我现在有 3 个 ThreadLocal  对象,2 个线程。

ThreadLocal<String> threadLocal1 =  new ThreadLocal<>();
ThreadLocal<Integer> threadLocal2 =  new ThreadLocal<>();
ThreadLocal<Integer> threadLocal3 =  new ThreadLocal<>();

那此时 ThreadLocal  对象和线程的关系如下图所示:


image.png


这样一来就满足了本地化资源的需求,每个线程维护自己的变量,互不干扰,实现了变量的线程隔离,同时也满足存储多个本地变量的需求,完美!

JDK就是这样实现的!我们来看看源码。


从源码看ThreadLocal 的原理


前面我们说到 Thread 对象里面会有个 map,用来保存本地变量。

我们来看下 jdk 的 Thread 实现

public class Thread implements Runnable {
     // 这就是我们说的那个 map 。
    ThreadLocal.ThreadLocalMap threadLocals = null;
}
复制代码

可以看到,确实有个 map ,不过这个 map 是 ThreadLocal 的静态内部类,记住这个变量的名字 threadLocals,下面会有用的哈。

看到这里,想必有很多小伙伴会产生一个疑问。

竟然这个 map 是放在 Thread 里面使用,那为什么要定义成 ThreadLocal 的静态内部类呢?

首先内部类这个东西是编译层面的概念,就像语法糖一样,经过编译器之后其实内部类会提升为外部顶级类,和平日里外部定义的类没有区别,也就是说在 JVM 中是没有内部类这个概念的

一般情况下非静态内部类用在内部类,跟其他类无任何关联,专属于这个外部类使用,并且也便于调用外部类的成员变量和方法,比较方便。

而静态外部类其实就等于一个顶级类,可以独立于外部类使用,所以更多的只是表明类结构和命名空间

所以说这样定义的用意就是说明 ThreadLocalMap 是和 ThreadLocal 强相关的,专用于保存线程本地变量。

现在我们来看一下 ThreadLocalMap 的定义:


image.png


重点我已经标出来了,首先可以看到这个 ThreadLocalMap 里面有个 Entry 数组,熟悉 HashMap 的小伙伴可能有点感觉了。

这个 Entry 继承了 WeakReference 即弱引用。这里需要注意,不是说 Entry 自己是弱引用,看到我标注的 Entry 构造函数的 super(k) 没,这个 key 才是弱引用。

所以 ThreadLocalMap 里有个 Entry 的数组,这个 Entry 的 key 就是 ThreadLocal 对象,value 就是我们需要保存的值。

那是如何通过 key 在数组中找到 Entry 然后得到 value 的呢 ?

这就要从上面的 threadLocalName.get()说起,不记得这个代码的滑上去看下示例,其实就是调用 ThreadLocal 的 get 方法。

此时就进入 ThreadLocal#get 方法中了,这里就可以得知为什么不同的线程对同一个 ThreadLocal 对象调用 get 方法竟然能得到不同的值了。


image.png


这个中文注释想必很清晰了吧!ThreadLocal#get 方法首先获取当前线程,然后得到当前线程的 ThreadLocalMap 变量即 threadLocals,然后将自己作为 key 从 ThreadLocalMap 中找到 Entry ,最终返回 Entry 里面的 value 值。

这里我们再看一下 key 是如何从 ThreadLocalMap 中找到 Entry 的,即map.getEntry(this)是如何实现的,其实很简单。


image.png


可以看到 ThreadLocalMap 虽然和 HashMap 一样,都是基于数组实现的,但是它们对于 Hash 冲突的解决方法不一样,HashMap 是通过链表(红黑树)法来解决冲突,而 ThreadLocalMap 是通过开放寻址法来解决冲突。

听起来好像很高级,其实道理很简单,我们来看一张图就很清晰了。


image.png



相关文章
|
3月前
|
Java 测试技术 索引
ThreadLocal详解
文章详细讨论了Java中的`ThreadLocal`,包括它的基本使用、定义、内部数据结构`ThreadLocalMap`、主要方法(set、get、remove)的源码解析,以及内存泄漏问题和避免策略。`ThreadLocal`提供了线程局部变量,确保多线程环境下各线程变量的独立性,但不当使用可能导致内存泄漏,因此建议在不再需要`ThreadLocal`变量时调用其`remove`方法。
112 2
ThreadLocal详解
|
4月前
|
存储 Java
ThreadLocal应用及理解
ThreadLocal应用及理解
47 10
|
6月前
|
存储 Java 数据管理
ThreadLocal的使用
`ThreadLocal`是Java中的线程局部变量工具,确保每个线程都有自己的变量副本,互不干扰。适用于保持线程安全性数据和跨方法共享数据。基本用法包括创建实例、设置和获取值以及清除值。例如,创建ThreadLocal对象后,使用`.set()`设置值,`.get()`获取值,`.remove()`清除值。注意ThreadLocal可能引起内存泄漏,应适时清理,并谨慎使用以避免影响代码可读性和线程安全性。它是多线程编程中实现线程局部数据管理的有效手段。
89 10
|
6月前
|
存储 Java
ThreadLocal 有什么用
ThreadLocal 有什么用
52 0
|
存储 算法 安全
深入详解ThreadLocal
在我们日常的并发编程中,有一种神奇的机制在静悄悄地为我们解决着各种看似棘手的问题,它就是 ThreadLocal 。
21513 9
深入详解ThreadLocal
|
11月前
|
存储
ThreadLocal
ThreadLocal
52 0
|
存储 Java
|
存储 Java
ThreadLocal相关使用
ThreadLocal相关使用
192 0
ThreadLocal相关使用
|
存储 Java
ThreadLocal理解
ThreadLocal理解
285 0
ThreadLocal理解