ThreadLocal详细笔记
- 一、ThreadLocal的基本概念
- 二、ThreadLocal的独特性
-
- [2.1 数据访问方式](#2.1 数据访问方式)
- [2.2 线程安全实现](#2.2 线程安全实现)
- [2.3 适用场景](#2.3 适用场景)
- [三、ThreadLocal 的简单使用](#三、ThreadLocal 的简单使用)
- [四、ThreadLocal 的工作原理](#四、ThreadLocal 的工作原理)
- 五、ThreadLocal和内存泄漏的关系
-
- [5.1 ThreadLocalMap的Entry的Key设计成弱引用](#5.1 ThreadLocalMap的Entry的Key设计成弱引用)
- [5.2 弱引用会导致内存泄漏](#5.2 弱引用会导致内存泄漏)
- [六、ThreadLocal 的注意事项](#六、ThreadLocal 的注意事项)
-
- [6.1 线程复用问题](#6.1 线程复用问题)
- [6.2 初始化值的正确设置](#6.2 初始化值的正确设置)
- [6.3 理解线程局部性的范围](#6.3 理解线程局部性的范围)
- [6.4 适当的命名和文档说明](#6.4 适当的命名和文档说明)
一、ThreadLocal的基本概念
ThreadLocal 属于Java中能在多线程环境下存储线程局部变量的机制。作用在于给每个线程都准备单独的变量副本,通过这种方式防止线程之间出现竞争条件。
ThreadLocal 的工作方式是在每个线程内部创建一个独立的变量副本,而且每个线程仅仅可以访问属于自己的那个副本。
主要可以处理的多线程并发问题:
- 避免多线程对共享变量的并发访问冲突:比如在一个 Web 应用中,存储每个线程的用户会话信息等。每个线程都能独立地操作自己的 ThreadLocal 变量副本,而不需要担心其他线程对其的干扰。
- 简化代码中的线程安全问题处理:当某些对象或数据只在特定线程内使用,并且不希望受到其他线程的影响时,使用 ThreadLocal 可以更方便地管理这些数据,而不需要复杂的同步机制。
二、ThreadLocal的独特性
传统的共享变量方式与 ThreadLocal 有很大的不同,ThreadLocal 的独特性主要体现在:
2.1 数据访问方式
- 传统共享变量:
xml
多个线程可直接访问和修改同一个共享变量。需要使用同步机制(如synchronized关键字或锁)来确保数据的一致性和完整性,以避免数据竞争和不一致的情况。
例如,多个线程同时对一个共享计数器进行递增操作,如果不进行适当的同步,可能会导致计数器的值不准确。
所有线程都能看到共享变量的最新状态,任何一个线程对其的修改都会立即对其他线程可见。这在某些情况下是必要的,但增加复杂性,因为线程需要时刻考虑其他线程可能对共享变量的影响。
- ThreadLocal:
xml
每个线程都有自己独立的变量副本,线程只能访问自己的副本,不会直接影响其他线程的副本。可消除对复杂同步机制的需求,极大地简化多线程编程。
例如,在使用 ThreadLocal 存储每个线程的用户会话信息时,不同线程可以独立地修改自己的会话信息,而不用担心与其他线程的冲突。
每个线程的变量副本对于该线程是私有的,修改自己的副本不会影响其他线程的副本状态,各个线程之间的数据相互隔离。
2.2 线程安全实现
- 传统共享变量:
xml
要保证线程安全,需要开发者谨慎地使用同步代码块或方法,确保在并发访问时正确地协调对共享变量的读写操作。这不仅需要深入理解多线程同步的原理,还容易出现死锁、活锁等并发问题。
例如,使用synchronized关键字不当可能导致线程长时间等待锁,从而影响程序性能和响应性。
对于复杂的数据结构作为共享变量,还需要考虑更高级的并发控制策略,如使用读写锁等,以提高并发性能。
- ThreadLocal:
xml
由于每个线程都操作自己独立的变量副本,从本质上就保证线程安全。不需要额外的同步代码来保护变量的读写操作,减少因同步带来的性能开销和潜在的并发问题。
开发者可以更专注于单个线程内的业务逻辑,而不必过多担心多线程并发访问的复杂性。
2.3 适用场景
- 传统共享变量:
xml
适用于需要多个线程共享数据并进行协作的场景。
比如在一个生产者-消费者模型中,生产者和消费者线程需要共享一个队列来传递数据。这种情况下,共享变量作为数据传递和协作的媒介是必要的。
在一些需要全局状态管理的系统中,共享变量可以用来存储系统的配置信息等,供所有线程读取和使用(在修改时需要注意同步)。
- ThreadLocal:
xml
常用于需要保存每个线程自身状态的情况。
例如在 Web 应用中,每个线程处理不同的用户请求,使用 ThreadLocal 可以方便地保存每个用户请求相关的上下文信息,如用户身份认证信息、事务状态等。
这样不同的请求线程可以独立地维护自己的状态,互不干扰。
在一些日志记录框架中,ThreadLocal 可以用来保存每个线程的日志级别等配置信息,使得不同线程可以根据自己的需求进行独立的日志记录,而不需要在每次记录日志时都传递相关配置信息。
总而言之,ThreadLocal 以其独特的数据隔离和线程安全特性,为多线程编程中处理线程局部数据提供一种简洁而有效的方式,与传统共享变量方式对比,在特定的应用场景中具有很大的优势。
三、ThreadLocal 的简单使用
以日期转换工具类为例,使用ThreadLocal。
在多线程中进行日期的格式转换:
java
public class DateUtil {
private static final SimpleDateFormat simpleDateFormat =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static Date parse(String dateString) {
Date date = null;
try {
date = simpleDateFormat.parse(dateString);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 50; i++) {
executorService.execute(()->{
System.out.println(DateUtil.parse("2024-08-08 20:20:20"));
});
}
executorService.shutdown();
}
}
报错如下:
java
Exception in thread "pool-1-thread-6" Exception in thread "pool-1-thread-23" Exception in thread "pool-1-thread-21" Exception in thread "pool-1-thread-20" java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at org.example.threadLocal.DateUtil.parse(DateUtil.java:19)
at org.example.threadLocal.DateUtil.lambda$main$0(DateUtil.java:31)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
因为SimpleDateFormat不是线性安全的,并发多线程场景下即会报错,不同的线程都能基于初始值进行独立计算。
使用ThreadLocal对SimpleDateFormat进行处理:
java
public class SafeDateUtil {
private static final ThreadLocal<SimpleDateFormat> simpleDateFormat =
ThreadLocal.withInitial(
()->new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static Date parse(String dateString) {
Date date = null;
try {
date = simpleDateFormat.get().parse(dateString);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 50; i++) {
executorService.execute(()->{
System.out.println(SafeDateUtil.parse("2024-08-08 20:20:20"));
});
}
executorService.shutdown();
}
}
结果如下:
java
Thu Aug 08 20:20:20 CST 2024
Thu Aug 08 20:20:20 CST 2024
Thu Aug 08 20:20:20 CST 2024
Thu Aug 08 20:20:20 CST 2024
Thu Aug 08 20:20:20 CST 2024
Thu Aug 08 20:20:20 CST 2024
Thu Aug 08 20:20:20 CST 2024
Thu Aug 08 20:20:20 CST 2024
Thu Aug 08 20:20:20 CST 2024
Thu Aug 08 20:20:20 CST 2024
四、ThreadLocal 的工作原理
ThreadLocal的内存结构图:
Thread类中,有个ThreadLocal.ThreadLocalMap 的成员变量。
ThreadLocalMap内部维护了Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型对象值。
源码中:
Thread线程类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap。
ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是ThreadLocal的泛型值。
并发场景下,每个线程Thread,在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而可以实现了线程隔离。
ThreadLocal类中的关键set()方法:
java
public void set(T value) {
Thread t = Thread.currentThread(); //获取当前线程t
ThreadLocalMap map = getMap(t); //根据当前线程获取到ThreadLocalMap
if (map != null) //如果获取的ThreadLocalMap对象不为空
map.set(this, value); //K,V设置到ThreadLocalMap中
else
createMap(t, value); //创建一个新的ThreadLocalMap
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; //返回Thread对象的ThreadLocalMap属性
}
void createMap(Thread t, T firstValue) { //调用ThreadLocalMap的构造函数
t.threadLocals = new ThreadLocalMap(this, firstValue); // this表示当前类ThreadLocal
}
ThreadLocal类中的关键get()方法:
java
public T get() {
Thread t = Thread.currentThread();//获取当前线程t
ThreadLocalMap map = getMap(t);//根据当前线程获取到ThreadLocalMap
if (map != null) { //如果获取的ThreadLocalMap对象不为空
//由this(即ThreadLoca对象)得到对应的Value,即ThreadLocal的泛型值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue(); //初始化threadLocals成员变量的值
}
private T setInitialValue() {
T value = initialValue(); //初始化value的值
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); //以当前线程为key,获取threadLocals成员变量,它是一个ThreadLocalMap
if (map != null)
map.set(this, value); //K,V设置到ThreadLocalMap中
else
createMap(t, value); //实例化threadLocals成员变量
return value;
}
五、ThreadLocal和内存泄漏的关系
5.1 ThreadLocalMap的Entry的Key设计成弱引用
在 Java 中,弱引用对象只要垃圾回收器运行,就会被回收,不管内存是否充足。比如ThreadLocal中ThreadLocalMap的Key用弱引用,就是为在Key没有其他强引用的时候,能尽快被回收,防止因为一直保留不需要的Key而导致内存泄漏。
- 防止内存泄漏:
当ThreadLocal对象作为ThreadLocalMap中Entry的Key时,如果Key是强引用,那么即使在外部没有对ThreadLocal对象的强引用(例如,在方法调用结束后,局部的ThreadLocal变量不再被使用),但由于ThreadLocalMap中Entry的Key对它有强引用,ThreadLocal对象及其关联的值将无法被垃圾回收。
这可能会导致内存泄漏,尤其是在长期运行的应用程序中,如果有大量的线程创建和销毁,而这些ThreadLocal对象没有被正确清理,就会占用不必要的内存。
通过将Key设计成弱引用,当没有其他强引用指向ThreadLocal对象时,垃圾回收器可以回收该ThreadLocal对象,从而避免这种内存泄漏问题。 - 符合线程局部变量的生命周期特点:
线程局部变量ThreadLocal的生命周期通常与创建它的线程相关。
当线程结束时,理论上它的所有线程局部变量都应该可以被回收。
然而,如果Key是强引用,即使线程结束了,但ThreadLocalMap可能仍然持有对ThreadLocal对象的引用,导致相关资源无法及时释放。
设计成弱引用可以使得在合适的时候(当没有其他强引用时)自动清理不再使用的ThreadLocal对象及其关联的值,更符合线程局部变量的实际生命周期管理需求。 - 避免无用数据的累积:
在多线程环境下,如果不将Key设计成弱引用,随着线程的创建和销毁,可能会有很多不再使用的ThreadLocal对象残留在ThreadLocalMap中,导致ThreadLocalMap占用的内存不断增加。
使用弱引用Key可以在一定程度上自动清理这些无用的数据,保持内存的合理使用。
例如,如果Key不是弱引用,可能导致的问题:
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadLocalMemoryLeakExample {
static final int THREAD_COUNT = 5;
public static void main(String[] args) {
// 创建固定线程数的线程池
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
executorService.submit(new Runnable() {
@Override
public void run() {
// 创建一个 ThreadLocal 对象
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("Some value for this thread");
// 这里假设执行一些其他操作后,threadLocal 变量在方法内不再被使用,但由于 Key 是强引用,它可能无法被回收
System.out.println(Thread.currentThread().getName() + " set value in ThreadLocal");
}
});
}
// 关闭线程池
executorService.shutdown();
}
}
可见,如果ThreadLocal的Key不是弱引用,那么每次循环创建的ThreadLocal对象即使在任务执行完毕后可能也无法被回收,因为ThreadLocalMap中的Entry一直持有对它的强引用。而将Key设计成弱引用可以在合适的时候让这些不再使用的ThreadLocal对象被垃圾回收。
因此,将ThreadLocalMap中Entry的Key设计成弱引用是为了更好地管理内存,防止内存泄漏,并符合线程局部变量的生命周期特点。
5.2 弱引用会导致内存泄漏
TreadLocal的引用示意图:
hreadLocalMap使用ThreadLocal的弱引用作为key,当ThreadLocal变量被手动设置为null,即一个ThreadLocal没有外部强引用来引用它,当系统GC时,ThreadLocal一定会被回收。这样的话,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如线程池的核心线程),这些key为null的Entry的value就会一直存在一条强引用链:
java
Thread变量 -> Thread对象 -> ThreaLocalMap -> Entry -> value -> Object
对象永远无法回收,造成内存泄漏。
实际上,ThreadLocalMap的设计中已经考虑到这种情况。所以有一些防护措施:即在ThreadLocal的get,set,remove方法,都会清除线程ThreadLocalMap里所有key为null的value。
set方法:
get方法:
六、ThreadLocal 的注意事项
综上,在使用ThreadLocal时需要注意以下几点:
6.1 线程复用问题
在一些应用服务器或者线程池的环境中,线程可能会被复用。如果在一个线程中使用了ThreadLocal,并且没有正确清理,当下次这个线程被复用执行其他任务时,可能会得到上一次遗留下来的数据,从而导致错误的结果。所以在这种情况下,更要确保在合适的时机(比如任务执行结束时)清理ThreadLocal的数据。
6.2 初始化值的正确设置
如果使用ThreadLocal的withInitial方法来设置初始值,要确保初始值的计算是线程安全的。因为这个初始值的供应函数只会在每个线程第一次获取ThreadLocal的值时被调用,如果这个供应函数内部有不安全的操作,可能会导致问题。例如:
java
ThreadLocal<Integer> counter = ThreadLocal.withInitial(() -> {
// 这里如果有不安全的操作,可能会有问题
return someSharedVariable++;
});
这里someSharedVariable的自增操作如果没有正确同步,在多线程环境下会导致错误的初始值。
6.3 理解线程局部性的范围
ThreadLocal变量只在当前线程内是局部的。不要错误地认为它在整个应用程序中都是隔离的。不同线程之间的ThreadLocal变量是相互独立的,但是在同一个线程内,对ThreadLocal变量的修改会影响到该线程后续对这个变量的使用。比如,在一个线程中修改ThreadLocal的值,那在该线程的后续代码中获取到的就是修改后的值。
6.4 适当的命名和文档说明
使用ThreadLocal时,应该起一个有意义的名称,并且在代码中做好说明,以便其他开发者能够清楚地理解这个ThreadLocal变量的用途和生命周期管理的重要性。