🦊 Java
1、Java基础
1.1 StringBuffer和StringBuilder的区别
1.2 ConcurrentHashMap和TreeMap实现原理
1.3 ArrayList和LinkedList实现原理
1.4 HashSet和TreeSet实现原理
1.5 深拷贝与浅拷贝
1.6 抽象类与接口
2、Java多线程
2.1 并发编程的三大特性
2.2 指令重排
2.3 Volatile原理
2.4 CAS原理
2.5 Java的4种引用级别
2.6 Java中的锁
2.7 Synchronized实现原理
2.8 线程池实现原理
2.9 AQS
2.10 创建线程的方式
2.11 ThreadLocal原理
3、JVM
3.1 判断对象是否存活的方法
3.2 JVM内存结构
3.3 常见的垃圾收集算法有哪些
3.4 指针碰撞和空闲列表
3.5 常见的垃圾收集器有哪些
3.6 内存溢出与内存泄漏的区别
3.7 常用的JVM启动参数有哪些
3.8 反射机制
4、NIO
4.1 概述
5、Spring
5.1 Spring IOC
5.2 Spring AOP
6、SpringBoot
6.1 SpringBoot、SpringCloud的联系与区别
-
+
游客
注册
登录
ThreadLocal原理
## 背景 软件开发过程中并发是很重要的手段,引入多线程开发之后,自然要考虑好[同步](https://notebook.ricear.com/doc/325)、互斥、线程安全等内容。线程安全的实现方式有三种,分别为**互斥同步**、**非阻塞同步**、**无同步**三种: - 互斥同步:也称为阻塞同步,是一种最常见的并发正确性保障手段。同步是指在多线程并发访问数据时,保证共享数据在同一时刻只能被一个线程访问。互斥是实现同步的一种手段,主要的互斥实现方式有[临界区](https://notebook.ricear.com/doc/325)、[互斥量](https://notebook.ricear.com/doc/325)和[信号量](https://notebook.ricear.com/doc/325)等。互斥同步最主要的问题是线程阻塞和唤醒所带来的性能问题。互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。 ![](https://notebook.ricear.com/media/202312/2023-12-10_111825_7723820.09810970478196712.png) - 非阻塞同步:基于冲突检测的乐观并发策略,先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。非阻塞同步能够实现的前提是冲突检测和数据操作这两个步骤具有[原子性](https://notebook.ricear.com/doc/526),这点是通过硬件指令集来实现的,常见的乐观锁指令包括[检查并设置](https://en.wikipedia.org/wiki/Test-and-set)(Test-and-Set, TAS)、[两次检查并设置](https://en.wikipedia.org/wiki/Test_and_test-and-set)(Test-and-Test-and-Set, TTAS)、[获取并增加](https://en.wikipedia.org/wiki/Fetch-and-add)(Fetch-and-ADD, FAA)、[比较并交换](https://notebook.ricear.com/doc/529)(Compare-and-Swap, CAS)、[加载链接/条件存储](https://en.wikipedia.org/wiki/Load-link/store-conditional)(Load-Linked/Store-Conditional)等。 - 无同步:要保证线程安全,也并非一定要进行互斥同步或非阻塞同步,同步与线程安全两者没有必然的联系。同步只是保证共享数据在多线程争用时的正确性保障手段,如果一个方法本身就不涉及共享数据,那么也就不需要任何同步措施去保证其正确性,因此会有一些代码天生就是安全的,常见的主要有两类,一类是[可重入代码](https://baike.baidu.com/item/%E5%8F%AF%E9%87%8D%E5%85%A5%E4%BB%A3%E7%A0%81),另一类是[线程本地存储](https://en.wikipedia.org/wiki/Thread-local_storage)。 > :thinking: 什么是可重入代码? > > 可重入代码是指当一个程序正在运行时,执行线程可以再次进入并执行它,仍然可以得到符合设计时所预期的结果。与多线程并发执行的线程安全不同,可重入强调对单一线程执行时重新进入同一个子程序仍然是安全的。 > > 若一个函数是可重入的,则该函数应当满足下述条件: > > - 不能含有静态全局变量。 > - 不能返回静态全局变量的地址。 > - 只能处理由调用者提供的数据。 > - 不能依赖于单例模式资源的锁。 > - 调用的函数也必须是可重入的。 > > 上述条件就是要求可重入函数使用的所有变量都保存在调用[堆栈](https://notebook.ricear.com/doc/541)的当前[函数栈](https://notebook.ricear.com/doc/541)上,因此同一执行线程重入执行该函数时加载了新的函数帧,与前一次执行该函数时所使用的函数帧不冲突、不互相覆盖,从而保证了可重入执行安全。 > > 在以下代码中,函数`f`和函数`g`都是不可重入的。 > > ```c++ > int g_var = 1; > int f() { g_var = g_var + 2; return g_var; } > int g() { return f() + 2; } > ``` > > 以上代码中,`f`使用了全局变量`g_var`,所以,如果两个线程同时执行`f`并访问`g_var`,则返回的结果取决于执行的时间。因此,`f`不可重入。而`g`调用了`f`,所以`g`也不可重入。 > > 稍作修改后,两个函数都是可重入的。 > > ```c++ > int f(int i) { return i + 2; } > int g(int i) { return f(i) + 2; } > ``` > :thinking: 可重入与线程安全的关系是什么? > > 可重入与线程安全都关系到函数处理资源的方式,但是二者有很大的区别。 > > - **可重入会影响函数的外部接口,而线程安全只关心函数的实现**。 > - 大多数情况下,要将不可重入函数改为可重入的,需要修改函数接口,使得所有的数据都通过函数的调用者实现。 > - 要将非线程安全的函数改为线程安全的,则只需要修改函数的实现部分。一般通过加入同步机制以保护共享的资源,使之不会被几个线程同时访问。 > - **操作系统背景与 CPU 调度策略**: > - 可重入是在单线程操作系统背景下,重入的函数或者子程序,按照后进先出的线性序依次执行完毕。 > - 多线程执行的函数或子程序,各个线程执行的时机是由操作系统调度,不可预期的,但是该函数的每个执行线程都会不时的获得 CPU 的时间片,不断向前推进执行进度。 > - **可重入函数未必是线程安全的,线程安全函数未必是可重入的**: > - 例如,一个函数打开某个文件并读入数据。这个函数是可重入的,因为它的多个实例同时执行不会造成冲突;但它不是线程安全的,因为在它读入文件时可能由别的线程正在修改该文件,为了线程安全必须对文件加“同步锁”。 > - 另一个例子,函数在它的函数体内部访问共享资源使用了加锁、解锁操作,所以它是线程安全的,但是却不可重入。因为若该函数一个实例运行到已经执行加锁但未执行解锁时被停了下来,系统又启动该函数的另一个实例,则新的实例在加锁处将转入等待。如果该函数是一个中断处理服务,在中断处理时又发生新的中断将导致资源死锁。 > > 下述例子,是线程安全的,但不是可重入的。 > > ```c++ > int function() > { > mutex_lock(); > ... > function body > ... > mutex_unlock(); > } > ``` > > 多线程执行时,获得了互斥锁的线程总能获得 CPU 时间片,向前推进执行进度,最终解开互斥锁,使得别的线程也能获得互斥锁进入[临界区](https://notebook.ricear.com/doc/325)。但是,如果在单线程背景下第一次执行该函数时已经获得互斥锁进入临界区,这时该函数被重入执行,这将在重新申请互斥锁时被饿死,因为获得了互斥锁的该函数的第一次执行将永远没有机会再获得CPU 时间片。 > > :thinking: 什么是线程本地存储? > > 这个就是我们本文要讨论的内容,下面将会进行详细介绍。 ## 含义 线程本地存储又叫线程局部存储,英文名称为 Thread Local Storage,简称 TLS。实质上是**线程私有的全局变量**。普通全局变量在多线程中是共享的,一个线程对其进行了修改,其它所有线程都可以看到。而线程私有的全局变量与普通的全局变量不同,线程私有的全局变量是线程的私有财产,每个线程都有自己的一个副本,某个线程对其所做的修改只会修改到自己的副本,并不会修改到其它线程的副本。 > :bulb: 线程本地存储的来源。 > > 在单线程模型下,变量定义有两个维度,分别为在何处定义以及它的修饰属性(static/extern/auto/register 等)。extern 属性表示声明一个变量,与定义无关,在此不做讨论;而 register 是将变量优化成寄存器里面,这里也不做讨论。这样与变量定义相关的修饰属性就只有 auto 和 static 了,这两个维度可以得到变量的类型以及行为。一个变量的行为,可以从可见性和生命周期两个方面进行评定。变量的定义、属性、类型、生命周期和行为如下面的表格所示。 > > | 何处定义 | 修饰属性 | 变量类型 | 生命周期 | 可见性 | > | -------- | -------- | ------------ | -------------------------- | --------------- | > | 在函数外 | auto | 全局变量 | 整个程序 | 全局可见 | > | 在函数外 | static | 全局静态变量 | 整个程序 | 同一.c 文件可见 | > | 在函数内 | auto | 局部变量 | 函数开始执行到结束这段时间 | 函数内可见 | > | 在函数内 | static | 静态局部变量 | 整个程序 | 函数内可见 | > > 对于静态局部变量会有这样的疑问,它只能在函数内使用,为什么它的生命周期是全局的。其实它与局部变量是不一样的,局部变量在函数开始执行时,才创建出来,在函数执行结束时,它就消失了,不存在了。而静态局部变量,在函数没有执行时,它就已经存在了,这就是为什么说它是整个程序的生命周期。当函数执行结束之后,它仍然存在,下次再执行该函数时,它是基于之前执行后的结果开始执行的。所以静态局部变量与全局变量是一样的,不同之处在于只能在函数内才能对它进行访问。 > > 在单线程中,这一切都协调得很好。因为整个进程只有一个执行单元,任何时刻只有一个执行代码(或者函数)在访问同一变量。然而在多线程模式下,这一切都发生了变化,因为会同时出现多个执行单元,也即同一个函数会同时在多个执行单元中运行。 > > 在多线程中,我们可以通过加锁来保护数据,但这不是充分必要的。局部变量(生命周期在函数内)不受多线程影响,因为每个线程栈是独立的,多个线程同时执行该函数,访问局部变量都是每个线程一份的,不会产生数据竞争。但对于生命周期为整个程序的全局变量、全局静态变量和静态局部变量就不一样了。多线程下多个执行单元同时访问该变量会出现数据竞争,在这种情况下,必须加锁来保证程序逻辑的正确性。然而加锁会使部分代码回到串行执行的时代,如果加锁很频繁,那整个进程能并发执行的代码比例并不高,使用多线程带来的加速比并不高,最重要的是无法根据处理器个数来动态伸缩。 > > 因此在多线程程序设计中,很多时候需要对数据进行划分,把不同的数据交给不同的线程处理,从而避免多线程中竞争访问数据。但是有一个问题值得考虑,那就是各个独立线程在执行函数时(通常是不同线程执行相同函数,只是他们访问的数据不同),这些函数的中间输出结果能否也是线程独立呢。如果能完全独立,那么多线程之间完全不需要互斥,伸缩性最优。这个问题有两个解决方法,第一种方法是这些函数的输入和输出全部用参数传递,但这种方法对编程要求很高,有时这样的设计往往不是最优的;第二种方法是将函数的中间输出结果使用全局变量来保存(不推荐全局变量满天飞,但在一个模块内的静态全局变量还是常有的),这样就需要加锁保护,效率低下。在这个场景下,每个线程都希望自己看到的全局变量是自己的,不想看到别人的,也不想别人对它有修改,它也不想修改别人的。因此全局变量也是个形式而已,最终目的是它可以**独立拥有一份**。 > > 那么要解决多线程下的高效编程问题,必须对原来的变量模型稍作修改,支持一个额外的属性,那就是变量是多执行单元(线程)共享还是独立拥有一份。 > > | 何处定义 | 修饰属性 | 多执行单元共享/独享 | 变量类型 | 生命周期 | 可见性 | > | -------- | -------- | ------------------- | ---------------- | -------------------------- | --------------- | > | 在函数外 | auto | 共享 | 全局变量 | 整个程序 | 全局可见 | > | 在函数外 | auto | 独享 | 全局线程变量 | 整个程序 | 全局可见 | > | 在函数外 | static | 共享 | 全局静态变量 | 整个程序 | 同一.c 文件可见 | > | 在函数外 | static | 独享 | 全局静态线程变量 | 整个程序 | 同一.c 文件可见 | > | 在函数内 | auto | 独享 | 局部变量 | 函数开始执行到结束这段时间 | 函数内可见 | > | 在函数内 | static | 共享 | 静态局部变量 | 整个程序 | 函数内可见 | > | 在函数内 | static | 独享 | 静态局部线程变量 | 整个程序 | 函数内可见 | > > 需要注意的是,局部变量本来就是线程函数内独享的,所以它没有共享这个属性值,整个程序生命周期的变量都有共享和独享这两个属性值。 > > 在单线程模式下,所有整个程序生命周期的变量都是只有一份,那是因为只有一个执行单元;而在多线程模式下,有些变量需要支持每个线程独享一份的功能。这种**每个线程独享的变量放到每个线程专有的存储区域**,所以称为线程本地存储,或者线程私有数据。 ThreadLocal 是 Java 中线程本地存储的一种实现方案,下面我们将对 ThreadLocal 的原理进行详细介绍。 ## 实现原理 ### 源码解析 #### 属性 ThreadLocal 主要有以下基本属性。 ```java // 当前 ThreadLocal 的 hashCode 由 nextHashCode() 计算而来,用于计算当前 ThreadLocal 在 ThreadLocalMap 中的索引位置 private final int threadLocalHashCode = nextHashCode(); // 哈希魔数,主要与斐波那契散列法和黄金分割有关 private static final int HASH_INCREMENT = 0x61c88647; // 返回计算出的下一个哈希值,其值为 i * HASH_INCREMENT,其中 i 代表调用次数 private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } // 全局原子整型,每调用一次 nextHashCode() 累加一次,保证了在一台机器中每个 ThreadLocal 的 ThreadLocalHashCode 是唯一的 private static AtomicInteger nextHashCode = new AtomicInteger(); ``` > :thinking: HASH_INCREMENT 有什么作用? > > HASH_INCREMENT 转化为十进制是 1640531527,2654435769 等于 $\frac{\sqrt5-1}2$ 乘以 2 的 32 次方, $\frac{\sqrt5-1}2$ 就是黄金分割数,近似为 0.618。2654435769 转换成 int 类型就是 -1640531527,也就是说 0x61c88647 可以理解为一个黄金分割数乘以 2 的 32 次方,可以**保证 nextHashCode 生成的哈希值均匀分布在 2 的幂次方上**,且小于 2 的 32 次方。 除了上面的基本属性外,还有一个重要的属性 ThreadLocalMap,详见[附录](#1)。 #### 方法 ##### set ThreadLocal 的 set 方法源码如下。 ```java /** * 为当前 ThreadLocal 对象关联 value 值 * * @param value 要存储在此线程的线程副本的值 */ public void set(T value) { // 返回当前ThreadLocal所在的线程 Thread t = Thread.currentThread(); // 返回当前线程持有的map ThreadLocalMap map = getMap(t); if (map != null) { // 如果 ThreadLocalMap 不为空,则直接存储<ThreadLocal, T>键值对 map.set(this, value); } else { // 否则,需要为当前线程初始化 ThreadLocalMap,并存储键值对 <this, firstValue> createMap(t, value); } } ``` set 方法的作用是把我们想要存储的 value 给保存进去,其流程为: 1. 先获取到当前线程的引用。 2. 利用这个引用获取到 ThreadLocalMap: 1. 如果 ThreadLocalMap 为空,则去创建一个新的 ThreadLocalMap。 2. 如果 ThreadLocalMap 不为空,则使用 ThreadLocalMap 的 set 方法将 value 添加到 ThreadLocalMap 中。 set 方法的时序图如下图所示。 ![img](https://notebook.ricear.com/media/202401/2024-01-21_104657_6568010.027006424983484822.png) 从上面的源码中我们可以看到它是通过当前线程对象来获取的 ThreadLocalMap,其源码如下。 ```java /** * 返回当前线程 thread 持有的 ThreadLocalMap * * @param t 当前线程 * @return ThreadLocalMap */ ThreadLocalMap getMap(Thread t) { return t.threadLocals; } ``` getMap 方法的作用主要是获取当前线程内的 ThreadLocalMap 对象,原来这个 ThreadLocalMap 是线程的一个属性,Thread 中相关源码如下。 ```java /** * ThreadLocal 的 ThreadLocalMap 是线程的一个属性,所以在多线程环境下 threadLocals 是线程安全的 */ ThreadLocal.ThreadLocalMap threadLocals = null; ``` 可以看出每个线程都有 ThreadLocalMap 对象,被命名为 threadLocals,默认为 null,所以每个线程的 ThreadLocalMap 都是隔离独享的。调用 ThreadLocalMap.set() 时,会把当前 ThreadLocal 对象作为 key,想要保存的对象作为 value,存入 ThreadLocalMap 中,其源码如下。 ```java /** * 在 map 中存储键值对<key, value> * * @param key threadLocal * @param value 要设置的 value 值 */ private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; // 计算 key 在数组中的下标 int i = key.threadLocalHashCode & (len - 1); // 遍历一段连续的元素,以查找匹配的 ThreadLocal 对象 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { // 获取该哈希值处的ThreadLocal对象 ThreadLocal<?> k = e.get(); // 键值ThreadLocal匹配,直接更改map中的value if (k == key) { e.value = value; return; } // 若 key 是 null,说明 ThreadLocal 被清理了,直接替换掉 if (k == null) { replaceStaleEntry(key, value, i); return; } } // 直到遇见了空槽也没找到匹配的ThreadLocal对象,那么在此空槽处安排ThreadLocal对象和缓存的value tab[i] = new Entry(key, value); int sz = ++size; // 如果没有元素被清理,那么就要检查当前元素数量是否超过了容量阙值(数组大小的三分之二),以便决定是否扩容 if (!cleanSomeSlots(i, sz) && sz >= threshold) { // 扩容的过程也是对所有的 key 重新哈希的过程 rehash(); } } ``` Thread、ThreadLocal 和 ThreadLocalMap 的关系如下所示。 ![img](https://notebook.ricear.com/media/202401/2024-01-21_105849_3756410.6832718759049687.png) ##### get ThreadLocal 的 get 方法源码如下。 ```java /** * 返回当前 ThreadLocal 对象关联的值 * * @return */ public T get() { // 返回当前 ThreadLocal 所在的线程 Thread t = Thread.currentThread(); // 从线程中拿到 ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) { // 从 map 中拿到 entry ThreadLocalMap.Entry e = map.getEntry(this); // 如果不为空,读取当前 ThreadLocal 中保存的值 if (e != null) { @SuppressWarnings("unchecked") T result = (T) e.value; return result; } } // 若 map 为空,则对当前线程的 ThreadLocal 进行初始化,最后返回当前的 ThreadLocal 对象关联的初值,即 value return setInitialValue(); } ``` setInitialValue 的源码如下。 ```java /** * 初始化 ThreadLocalMap 并返回 ThreadLocal 对象关联的初值 * * @return */ private T setInitialValue() { // 将 value 初始化为 null T value = initialValue(); Thread t = Thread.currentThread(); // 获取当前线程对应的 ThreadLocalMap: // 1. 如果 ThreadLocalMap 存在, 则存储对应的 ThreadLocal 和 value 对象; // 2. 如果 ThreadLocalMap 不存在, 则创建一个新的 ThreadLocalMap, 然后存储对应的 ThreadLocal 和 value 对象。 ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; } ``` get 方法的主要流程为: 1. 先获取到当前线程的引用。 2. 获取当前线程内部的 ThreadLocalMap: 1. 如果 ThreadLocalMap 存在,则获取当前 ThreadLocal 对应的 value 值。 2. 如果 ThreadLocalMap 不存在或者找不到对应的 value 值,则调用 setInitialValue 进行初始化。 get 方法的时序图如下图所示。 ![img](https://notebook.ricear.com/media/202401/2024-01-21_111339_8270530.3219010464373129.png) ##### remove ThreadLocal 的 remove 方法源码如下: ```java /** * 清理当前 ThreadLocal 对象关联的键值对 */ public void remove() { // 返回当前线程持有的 map ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) { // 从 map 中清理当前 ThreadLocal 对象关联的键值对 m.remove(this); } } ``` ThreadLocalMap 的 remove 方法源码如下: ```java /** * 清除 key 对应的键值对 */ private void remove(ThreadLocal<?> key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len - 1); // 通过 key 的哈希值找到当前 key 在 table 中的位置 for (Entry e = tab[i]; // 采用线性探测法找到 Entry 中 key 为当前对象 key 的元素 e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { e.clear(); // 清除对象的引用, this.referent = null; expungeStaleEntry(i); // 擦除某个下标的 Entry(置为 null, 可以回收), 同时检测出整个 Entry[] 表中 key 为 null 的 Entry 一并擦除, 同时重新调整索引 return; } } } ``` ThreadLocal 的 remove 方法的时序图如下所示: ![img](https://notebook.ricear.com/media/202402/2024-02-03_161255_0348480.03294724310074326.png) remove 方法的主要流程为: 1. 获取到当前线程的 ThreadLocalMap。 2. 调用 ThreadLocalMap 的 remove 方法,从 ThreadLocalMap 中清理当前 ThreadLocal 对象关联的键值对,这样 value 就可以被 GC 回收了。 ## 存在问题及解决方法 ### 脏数据 在线程池中使用 ThreadLocal 时,由于**线程池中对线程的管理都是采用线程复用的方法**,与线程绑定的类的静态属性 ThreadLocal 变量也会重用。如果在实现的线程 run() 方法体中不显式地调用 remove() 方法清理与线程相关的 ThreadLocal 信息,那么如果下一个线程不调用 set() 方法设置初始值,就可能 get 到重用的线程信息,包括 ThreadLocal 所关联的线程对象的 value 值。具体示例如下。 ```java public class MyThread9 { public static final ThreadLocal<AtomicInteger> sequencer = ThreadLocal.withInitial(() -> new AtomicInteger(1)); public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(2); for (int i = 0; i < 4; i++) { executorService.submit(new Task()); } executorService.shutdown(); } static class Task implements Runnable { @Override public void run() { AtomicInteger value = sequencer.get(); int valueBeforeIncrement = value.getAndIncrement(); System.out.println(String.format("当前线程为: %s, 线程对应的值为: %s", Thread.currentThread().getName(), valueBeforeIncrement)); } } } ``` 我们预想的线程对应的值应该都为 1,但实际的运行结果为: ```txt 当前线程为: pool-1-thread-1, 线程对应的值为: 1 当前线程为: pool-1-thread-2, 线程对应的值为: 1 当前线程为: pool-1-thread-2, 线程对应的值为: 2 当前线程为: pool-1-thread-1, 线程对应的值为: 2 ``` 这是因为按照道理来说一个线程使用完,ThreadLocalMap 是应该要被清空的,但是由于线程池里面的线程都是复用的,**在线程执行下一个任务时,其 ThreadLocal 对象并不会被清空,修改后的值带到了下一个任务**。解决思路有以下几种: 1. 第一次使用 ThreadLocal 对象时,总是先调用 set 方法设置初始值;或者如果 ThreadLocal 重写了initialValue 方法,使用 ThreadLocal 对象前先调用 remove 方法。 2. 使用完 ThreadLocal 对象后,总是调用其 remove 方法。 3. 使用自定义的线程池,执行新任务时总是清空 ThreadLocal。 这里我们采用第二种方法来解决,修改后的代码如下。 ```java public class MyThread9 { public static final ThreadLocal<AtomicInteger> sequencer = ThreadLocal.withInitial(() -> new AtomicInteger(1)); public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(2); for (int i = 0; i < 4; i++) { executorService.submit(new Task()); } executorService.shutdown(); } static class Task implements Runnable { @Override public void run() { AtomicInteger value = sequencer.get(); int valueBeforeIncrement = value.getAndIncrement(); System.out.println(String.format("当前线程为: %s, 线程对应的值为: %s", Thread.currentThread().getName(), valueBeforeIncrement)); sequencer.remove(); } } } ``` 修改后线程运行结果如下。 ```txt 当前线程为: pool-1-thread-1, 线程对应的值为: 1 当前线程为: pool-1-thread-2, 线程对应的值为: 1 当前线程为: pool-1-thread-1, 线程对应的值为: 1 当前线程为: pool-1-thread-2, 线程对应的值为: 1 ``` ## 应用场景 ### 每个线程独享(SimpleDateFormat) > 每个线程需要一个独享的对象,通常是工具类,典型的需要使用的类有 SimpleDateFormat 和 Random。 当多个线程共用一个 SimpleDateFormat 时,如果每一个线程都创建一遍 SimpleDateFormat 对象,太消耗内存;如果改为 static 多个线程共用,但由于[ SimpleDateFormat 类是线程不安全的](https://www.cnblogs.com/fnlingnzb-learner/p/13280493.html),在调用的时候可以使用锁来解决,但是效率太低。因此可以考虑使用 ThreadLocal 来解决该问题,给每个线程分配一个 SimpleDateFormat,在确保线程安全的前提下提高执行效率。具体示例如下。 ```java public class MyThread8 { public static ExecutorService threadPool = Executors.newFixedThreadPool(10); public static final ThreadLocal<SimpleDateFormat> safeFormatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); public static void main(String[] args) { for (int i = 0; i < 1000; i++) { int finalSeconds = i; threadPool.submit(new Runnable() { @Override public void run() { SimpleDateFormat format = safeFormatter.get(); Date date = new Date(1000 * finalSeconds); String dateStr = format.format(date); System.out.println(dateStr); } }); } threadPool.shutdown(); } } ``` ### 同一线程内所有方法共享(全局存储用户登录信息) > 每个线程内需要保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦。可以用 ThreadLocal 保存一些业务信息(用户ID、用户名、用户权限)等,这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。在线程生命周期内,都可以通过 ThreadLocal.get() 方法获取自己 set 的对象,避免了将这个对象作为参数传递的麻烦。 在系统设计中,通常在用户登陆之后会把用户信息保存在 Session 或者 Token 中,这样我们使用常规的方法去获取用户信息时会很困难。例如将用户信息存储在 Session 中,需要在接口参数中加上 HttpServletRequest 对象,然后调用 getSession 方法,而且每一个需要获取用户信息的接口都要加上这个参数才能获取 Session,这样实现起来就比较复杂。因此可以考虑使用 ThreadLocal,在拦截器中获取到保存的用户信息并存入 ThreadLocal,这样当前线程在任何地方如果需要获取用户信息只需要使用 ThreadLocal 的 get 方法就可以了,具体实现方法如下。 **ThreadLocal 类:** ```java public class UserInfoThreadHolder { // 保存用户对象的ThreadLocal private static final ThreadLocal<User> userThreadLocal = new ThreadLocal<>(); // 添加当前登录用户方法 public static void addCurrentUser(User user){ userThreadLocal.set(user); } public static User getCurrentUser(){ return userThreadLocal.get(); } // 防止内存泄漏 public static void remove(){ userThreadLocal.remove(); } } ``` **拦截器:** ```java public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { User user = new User(); user.setNickName("nickname"); UserInfoThreadHolder.addCurrentUser(user); return true; } // 避免内存泄露 @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserInfoThreadHolder.remove(); } } ``` **注册拦截器:** ```java @Configuration @ComponentScan(basePackages = "com.example.carrental") public class WebMvcConfig implements WebMvcConfigurer { // 注册自定义拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/user/wx/login");//开放登录路径 } } ``` **使用:** ```java @RestController @RequestMapping("/user") public class UserController { @RequestMapping("/test") public String test(){ return UserInfoThreadHolder.getCurrentUser().getNickName(); } } ``` ### 其他 - Spring 事务管理器。 - Spring MVC 的 RequestContextHolder。 ## 附录 ### <a id="1">ThreadLocalMap</a> #### 属性 ThreadLocalMap 是 ThreadLocal 的静态内部类,当一个线程有多个 ThreadLocal 时,需要一个容器来管理多个 ThreadLocal。 ThreadLocalMap 的作用就是管理线程中多个 ThreadLocal。 ```java static class ThreadLocalMap { /** * 键值对实体的存储结构 */ static class Entry extends WeakReference<ThreadLocal<?>> { // 当前线程关联的 value,这个 value 并没有用弱引用追踪 Object value; /** * 构造键值对 * * @param k k 作 key,作为 key 的 ThreadLocal 会被包装为一个弱引用 * @param v v 作 value */ Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } // 初始容量,默认 16,必须为 2 的幂 private static final int INITIAL_CAPACITY = 16; // 存储 ThreadLocal 的键值对实体数组,长度必须为 2 的幂 private Entry[] table; // ThreadLocalMap 元素数量 private int size = 0; // 扩容的阈值,默认是数组大小的 2/3 private int threshold; } ``` 从源码中可以看到 ThreadLocalMap 其实就是一个简单的 Map 结构,底层是数组,有初始化大小,也有扩容阈值。数组的元素是 Entry,**Entry 的 key 就是 ThreadLocal 的引用,value 是 ThreadLocal 的值**。ThreadLocalMap 解决哈希冲突的方式是[线性探测法](https://notebook.ricear.com/doc/805),如果发生冲突会继续寻找下一个空的位置。 > :thinking: 为什么 ThreadLocalMap 采用线性探测法解决哈希冲突,而不是像 HashMap 一样采用链地址法? > > - ThreadLocal 中有一个属性 HASH_INCREMENT = 0x61c88647,而 0x61c88647 是一个神奇的数字,可以让哈希码均匀的分布在 2 的 N 次方的数组里(即 Entry[] table)。 > - ThreadLocal 往往存放的数据量不会特别大,而且 key 是弱引用又会被垃圾回收,可以及时让数据量更小,这个时候线性探测法简单的结构会更节省空间,同时数组的查询效率也是非常高的,加上第一点的保障,冲突概率也低。 > - 因此 ThreadLocalMap 采用线性探测法解决哈希冲突,而不是像 HashMap 一样采用链地址法。 > :thinking: ThreadLocal 为什么被设计为弱引用? > > ThreadLocalMap 的内部类 Entry 被设计为实现了 WeakReference,Entry 用来存放数据。**在构造 Entry 对象时,将传进来的 ThreadLocal 对象包装成了真正的[弱引用](https://notebook.ricear.com/doc/530)对象,而 Entry 对象和内部的 value 对象本身是强引用的**。 > > 只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此**不一定会很快发现那些最具有弱引用的对象**。简单理解就是当垃圾回收时,如果该对象只被 WeakReference 对象的弱引用字段(T reference)所引用,而未被任何强类型的对象引用,那么,该弱引用的对象就会被回收。 > > 具体示例如下。 > > ```java > public class MyThread7 { > public static void main(String[] args) throws InterruptedException { > firstStack(); > System.gc(); > TimeUnit.SECONDS.sleep(1); > Thread thread = Thread.currentThread(); > System.out.println(thread); > } > private static A firstStack() { > A a = new A(); > System.out.println("value: " + a.get()); > return a; > } > private static class A { > private ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "in class A"); > public String get() { > return threadLocal.get(); > } > public void set(String value) { > threadLocal.set(value); > } > } > } > ``` > > ![image-20240121161616423](https://notebook.ricear.com/media/202401/2024-01-21_161523_7313830.8481669314581801.png) > > 如果 ThreadLocal 是强引用,那么发生 GC 时由于 ThreadLocalMap 还持有 ThreadLocal 的强引用,会导致 ThreadLocal 不会被回收,从而导致内存泄漏。如果 ThreadLocal 是弱引用时,当被置为 null 时会通过弱引用机制在下一次垃圾回收时回收,从而可以避免 ThreadLocal 导致的内存泄漏。 > > 但是 value 本身是通过强引用引入的,而 ThreadLocalMap 和线程的生命周期是一致的,这就导致如果不做任何处理,当线程资源长期不释放时,即使 ThreadLocal 本身由于弱引用机制已经回收掉了,但 value 还是驻留在线程的 ThreadLocalMap 的 Entry 中,即存在 key 为 null,但 value 却有值的无效 Entry,从而导致内存泄漏。 > > 但实际上,ThreadLocal 内部已经为我们做了一定的防止内存泄漏的工作,如下所示。 > > ```java > private int expungeStaleEntry(int staleSlot) { > Entry[] tab = table; > int len = tab.length; > > // 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; > } > ``` > > 上述方法的作用是**擦除某个下标的 Entry(置为 null,可以回收),同时检测出整个 Entry[] 表中 key 为 null 的 Entry 一并擦除,同时重新调整索引**。该方法在每次调用 ThreadLocal 的 get、set、remove方法时都会执行,即 ThreadLocal 内部已经为我们做了对 key 为 null 的 Entry 的清理工作。但是该工作是有触发条件的,需要调用相应方法,假如我们使用完之后不做任何处理是不会触发的。因此在业务开发中应该**及时调用 ThreadLocal.remove 方法清理无效的局部存储**。 #### 方法 ##### getEntry 其中每个 Thread 的 ThreadLocalMap 以 ThreadLocal 作为 key,保存自己线程的 value 副本,也就是**保存在每个线程中,并没有保存在 ThreadLocal 对象中**。 ThreadLocalMap.getEntry() 方法源码如下。 ```java /** * 返回 key 关联的键值对实体 * * @param key threadLocal * @return */ private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; // 若 e 不为空,并且 e 的 ThreadLocal 的内存地址和 key 相同,直接返回 if (e != null && e.get() == key) { return e; } else { // 从 i 开始向后遍历找到键值对实体 return getEntryAfterMiss(key, i, e); } } ``` ##### resize 当 ThreadLocalMap 中的 ThreadLocal 的个数超过阈值时就会开始扩容,扩容的源码如下。 ```java /** * 扩容,重新计算索引,标记垃圾值,方便 GC 回收 */ private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; int newLen = oldLen * 2; // 新建一个数组,按照2倍长度扩容 Entry[] newTab = new Entry[newLen]; int count = 0; // 将旧数组的值拷贝到新数组上 for (int j = 0; j < oldLen; ++j) { Entry e = oldTab[j]; if (e != null) { ThreadLocal<?> k = e.get(); // 若有垃圾值,则标记清理该元素的引用,以便GC回收 if (k == null) { e.value = null; } else { // 计算 ThreadLocal 在新数组中的位置 int h = k.threadLocalHashCode & (newLen - 1); // 如果发生冲突,使用线性探测往后寻找合适的位置 while (newTab[h] != null) { h = nextIndex(h, newLen); } newTab[h] = e; count++; } } } // 设置新的扩容阈值,为数组长度的三分之二 setThreshold(newLen); size = count; table = newTab; } ``` ThreadLocalMap 扩容的主要流程如下: 1. 新建一个数组,**长度为原来数组长度的 2 倍**。 2. 将旧数组拷贝到新数组中: 1. 如果**存在垃圾值**(key 为 null),则**将其对应的 value 置为空**,**以便垃圾回收**。 2. 如果不存在垃圾值,则**计算 ThreadLocal 在新数组中的位置**: 1. 如果不发生冲突,则直接将其放置在该位置上。 2. 如果**发生冲突**,**使用线性探测法往后寻找合适的位置**,并将其放置在合适的位置上。 3. **设置新的扩容阈值为当前数组长度的三分之二**。 ## 参考文献 - [一文搞懂 ThreadLocal 原理](https://www.cnblogs.com/wupeixuan/p/12638203.html)。 - [ThreadLocal原理](https://fhfirehuo.github.io/Attacking-Java-Rookie/Chapter07/ThreadLocal.html)。 - [Java & Android 集合框架 9 全网最全的 ThreadLocal 原理详细解析 —— 原理篇](https://juejin.cn/post/7166202551782604837)。 - [Java & Android 集合框架 #10 全网最全的 ThreadLocal 原理详细解析 —— 源码篇](https://juejin.cn/post/7166202801470013476)。 - [深度解析ThreadLocal原理](https://segmentfault.com/a/1190000037738029)。 - [深入理解 Java 之 ThreadLocal 工作原理。](https://allenwu.itscoder.com/threadlocal-source) - [线程安全及三种解决方案。](https://zhuanlan.zhihu.com/p/143811831) - [多线程安全的案例展示与解决方案](https://www.cnblogs.com/javazhizhe/p/17453744.html)。 - [线程安全的实现方法:互斥同步、非阻塞同步、无同步方案。](https://blog.51cto.com/u_15077539/3774765) - [java线程安全的实现方法(互斥同步,非阻塞同步等)](https://www.jianshu.com/p/d1cdeae4dce6)。 - [Java 并发简介](https://turnon.gitee.io/javacore/concurrent/java-concurrent-introduction.html)。 - [深入理解Java虚拟机笔记6——线程安全与锁优化](https://www.punklu.tech/post/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3java%E8%99%9A%E6%8B%9F%E6%9C%BA%E7%AC%94%E8%AE%B06%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E4%B8%8E%E9%94%81%E4%BC%98%E5%8C%96)。 - [Java 多线程并发【3】线程安全](https://juejin.cn/post/7100256218697957412)。 - [可重入代码](https://baike.baidu.com/item/%E5%8F%AF%E9%87%8D%E5%85%A5%E4%BB%A3%E7%A0%81)。 - [可重入](https://zh.wikipedia.org/wiki/%E5%8F%AF%E9%87%8D%E5%85%A5)。 - [聊聊Linux中的线程本地存储(1)——什么是TLS](https://blog.csdn.net/linyt/article/details/51931737)。 - [最后看一下什么是线程本地存储(TLS)](https://blog.csdn.net/luyaran/article/details/120698184)。 - [Java并发编程—线程本地存储模式:没有共享,就没有伤害](https://zhangquan.me/2023/07/26/java-bing-fa-bian-cheng-xian-cheng-ben-di-cun-chu-mo-shi-mei-you-gong-xiang-jiu-mei-you-shang-hai)。 - [谈谈ThreadLocal为什么被设计为弱引用](https://zhuanlan.zhihu.com/p/304240519)。 - [Java ThreadLocal 深入底层源代码; 讲清楚为什么 ThreadLocalMap 的 Entry 的 key 使用弱引用;](https://juejin.cn/post/7106734939386675236) - [ThreadLocal为什么要使用弱引用和内存泄露问题](https://blog.51cto.com/xiongmaoit/5429934)。 - [ThreadLocal弱引用与内存泄漏分析](https://zhuanlan.zhihu.com/p/91579723)。 - [被面试官问懵了,ThreadLocal的key为什么设置成弱引用?](https://blog.csdn.net/foxException/article/details/123496254) - [ThreadLocal的实现原理,ThreadLocal为什么使用弱引用](https://juejin.cn/post/7261599630827454520)。 - [Java-ThreadLocal三种使用场景](https://cloud.tencent.com/developer/article/1636025)。 - [ThreadLocal的使用场景及使用方式](https://blog.csdn.net/wufaliang003/article/details/118887557)。 - [Java11 ThreadLocal的remove()方法源码分析 ](https://www.cnblogs.com/east7/p/13893633.html)。 - [ThreadLocal常用方法、使用场景及注意事项说明](https://www.jb51.net/article/225045.htm)。
ricear
2024年2月3日 20:09
©
BY-NC-ND(4.0)
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
分享
链接
类型
密码
更新密码