ThreadLocal 课程笔记
一、章节结构概述
本章主要学习重要的工具类 ThreadLocal。章节分为六大模块:
- ThreadLocal 的两大使用场景
- ThreadLocal 所带来的好处
- ThreadLocal 的主要方法及使用顺序
- ThreadLocal 原理源码分析
- 使用 ThreadLocal 的注意点和使用规范
从下一个小节开始,将逐一展开这六大模块的内容。
二、两大使用场景
场景一:每个线程需要一个独享的对象
- 背景:某些工具类本身不是线程安全的,如果多个线程共享同一个静态工具类,会有很大风险,甚至肯定会出错。
- 解决方案:使用 ThreadLocal 为每个线程制作一个独享的对象。不同线程拥有的是不同实例,相互之间不会影响。
- 典型例子:SimpleDateFormat 和 Random 这两个工具类都是线程不安全的,使用 ThreadLocal 非常合适。
场景二:每个线程内需要保存一些全局信息
- 背景:在拦截器中获取到的用户信息,希望在同一个线程的不同方法中直接调用,避免每次传递参数的繁琐。
- 解决方案:利用 ThreadLocal 在线程内保存全局信息。例如,请求进来后在拦截器中将 token 转成用户信息并保存,后续该线程调用的其他方法(如买商品、更新库存、日志记录、抽奖等)可以直接获取用户信息,无需层层传递参数。
- 优势:简化代码,提高可读性和维护性。
这两种场景有明显区别:第一种场景侧重于解决工具类线程不安全问题,让每个线程有独立工具类实例;第二种场景主要解决参数传递麻烦的问题。
ThreadLocal 课程笔记
一、第一大使用场景:每个线程需要一个独享的对象
场景描述
每个线程需要一个独享的对象,这些对象通常是工具类实例。由于工具类本身不是线程安全的,多个线程共享同一个实例会有风险,因此需要为每个线程提供独立的实例。
比喻
一个班级有30个同学,只有一本教材。如果大家都抢着看,就会发生线程安全问题。使用ThreadLocal相当于复印30份教材,每人一份,互不影响。
ThreadLocal 的命名由来
ThreadLocal中的"Thread"代表线程,"Local"代表本地。每个线程只能访问自己的实例副本,其他线程无法访问,不存在多线程共享问题。
代码示例:SimpleDateFormat 的进化之路
第一步:两个线程,各自创建对象
java">package thread_local;
public class ThreadLocalNormalUsage00 {public static String date(long second) {// 将秒转换为毫秒Date date = new Date(second * 1000);// 定义日期格式SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// 格式化日期return dateFormat.format(date);}public static void main(String[] args) {// 线程1:打印10秒后的时间new Thread(() -> System.out.println(date(10))).start();// 线程2:打印1007秒后的时间new Thread(() -> System.out.println(date(1007))).start();}
}
第二步:扩展到多个线程
java">package thread_local;
public class ThreadLocalNormalUsage01 {public static String date(long second) {Date date = new Date(second * 1000);SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");return dateFormat.format(date);}public static void main(String[] args) {// 使用线程池ExecutorService threadPool = Executors.newFixedThreadPool(10);for (int i = 0; i < 1000; i++) {final long taskSecond = i;threadPool.submit(() -> {System.out.println(date(taskSecond));});}threadPool.shutdown();}
}
第三步:使用静态对象引发线程安全问题
java">package thread_local;
public class ThreadLocalNormalUsage02 {private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public static String date(long second) {Date date = new Date(second * 1000);return DATE_FORMAT.format(date);}public static void main(String[] args) {ExecutorService threadPool = Executors.newFixedThreadPool(10);for (int i = 0; i < 1000; i++) {final long taskSecond = i;threadPool.submit(() -> {System.out.println(date(taskSecond));});}threadPool.shutdown();}
}
第四步:使用 synchronized 解决线程安全问题
java">package thread_local;
public class ThreadLocalNormalUsage03 {private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public static String date(long second) {Date date = new Date(second * 1000);synchronized (ThreadLocalNormalUsage03.class) {return DATE_FORMAT.format(date);}}public static void main(String[] args) {ExecutorService threadPool = Executors.newFixedThreadPool(10);for (int i = 0; i < 1000; i++) {final long taskSecond = i;threadPool.submit(() -> {System.out.println(date(taskSecond));});}threadPool.shutdown();}
}
第五步:使用 ThreadLocal 解决问题
java">package thread_local;
public class ThreadLocalNormalUsage04 {private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT_THREAD_LOCAL = new ThreadLocal<SimpleDateFormat>() {@Overrideprotected SimpleDateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");}};public static String date(long second) {Date date = new Date(second * 1000);SimpleDateFormat dateFormat = DATE_FORMAT_THREAD_LOCAL.get();return dateFormat.format(date);}public static void main(String[] args) {ExecutorService threadPool = Executors.newFixedThreadPool(10);for (int i = 0; i < 1000; i++) {final long taskSecond = i;threadPool.submit(() -> {System.out.println(date(taskSecond));});}threadPool.shutdown();}
}
使用 Lambda 表达式简化 ThreadLocal 初始化
java">package thread_local;
public class ThreadLocalNormalUsage05 {private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT_THREAD_LOCAL = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));public static String date(long second) {Date date = new Date(second * 1000);SimpleDateFormat dateFormat = DATE_FORMAT_THREAD_LOCAL.get();return dateFormat.format(date);}public static void main(String[] args) {ExecutorService threadPool = Executors.newFixedThreadPool(10);for (int i = 0; i < 1000; i++) {final long taskSecond = i;threadPool.submit(() -> {System.out.println(date(taskSecond));});}threadPool.shutdown();}
}
总结
通过以上代码示例,我们逐步展示了如何使用ThreadLocal解决多线程环境下工具类实例的线程安全问题。最终方案不仅保证了线程安全,还避免了synchronized带来的性能损耗,实现了高效的内存利用。每个线程都有自己的独享对象,不同线程之间互不干扰,完美契合了ThreadLocal的设计初衷。
ThreadLocal 详细笔记
一、ThreadLocal 第二个使用场景:避免参数传递的麻烦
(一)场景描述
在实际开发中,一个请求可能需要调用多个方法,每个方法都需要使用到某些相同的对象(如用户信息)。如果每次都通过参数传递这些对象,会导致代码冗余且难以维护。
(二)传统解决方案的弊端
- 直接使用 static 变量:不可行,因为多个请求对应的用户信息不同,static 变量会导致数据混乱。
- 使用 Map:虽然可以存储每个线程的用户信息,但需要保证线程安全,使用 synchronized 或 ConcurrentHashMap 会影响性能。
(三)ThreadLocal 的优势
ThreadLocal 无需同步,也不需要 ConcurrentHashMap,可以在不影响性能的情况下,避免层层传递参数,直接达到共享对象的目的。
(四)代码示例:使用 ThreadLocal 避免参数传递
1. 创建 User 类
java">public class User {private String name;public User(String name) {this.name = name;}public String getName() {return name;}public void setName(String name) {this.name = name;}
}
2. 创建 ServiceOne 类
java">public class ServiceOne {public void process(User user) {UserContextHolder.setUser(user);}
}
3. 创建 UserContextHolder 类
java">public class UserContextHolder {private static final ThreadLocal<User> USER_THREAD_LOCAL = new ThreadLocal<>();public static void setUser(User user) {USER_THREAD_LOCAL.set(user);}public static User getUser() {return USER_THREAD_LOCAL.get();}
}
4. 创建 ServiceTwo 和 ServiceThree 类
java">public class ServiceTwo {public void process() {User user = UserContextHolder.getUser();System.out.println("ServiceTwo: " + user.getName());}
}public class ServiceThree {public void process() {User user = UserContextHolder.getUser();System.out.println("ServiceThree: " + user.getName());}
}
5. 创建主函数进行测试
java">public class ThreadLocalExample {public static void main(String[] args) {ServiceOne serviceOne = new ServiceOne();ServiceTwo serviceTwo = new ServiceTwo();ServiceThree serviceThree = new ServiceThree();User user = new User("超哥");serviceOne.process(user);serviceTwo.process();serviceThree.process();}
}
6. 运行结果
ServiceTwo: 超哥
ServiceThree: 超哥
(五)总结
在这个示例中,我们通过 ThreadLocal 在不同的方法之间共享了 User 对象,而无需通过参数传递。每个线程都有自己的 User 对象副本,避免了线程安全问题,同时提高了代码的可读性和维护性。这种方法特别适用于需要在多个方法中共享某些对象的场景,如用户信息、事务 ID 等。
二、知识点总结
- ThreadLocal 的第二个使用场景:避免参数传递的麻烦。
- 传统解决方案的弊端:
- 使用 static 变量会导致数据混乱。
- 使用 Map 需要保证线程安全,影响性能。
- ThreadLocal 的优势:无需同步,不影响性能,避免层层传递参数。
- User 类:定义用户信息的类,包含姓名等属性。
- ServiceOne 类:模拟业务场景,将用户信息设置到 ThreadLocal 中。
- UserContextHolder 类:持有 ThreadLocal 实例,提供设置和获取用户信息的方法。
- ServiceTwo 和 ServiceThree 类:模拟业务场景,从 ThreadLocal 中获取用户信息并使用。
- 主函数测试:创建服务实例,模拟用户请求,验证 ThreadLocal 的使用效果。
- 运行结果:验证了通过 ThreadLocal 成功在不同方法间共享用户信息。
ThreadLocal 课程笔记
一、ThreadLocal 的重要方法解析
(一)initialValue 方法
- 作用 :返回当前线程对应的初始值,且该方法是延迟加载的,只有在调用 get 方法时才会触发。
- 源码示例 :在 ThreadLocal 的 get 方法中,若检测到值未设置,则会调用 initialValue 方法来初始化值。
- 特点 :
- 若未重写 initialValue 方法,默认返回 null。在需要初始值的场景下,必须重写该方法。
- 第一次调用 get 方法时会间接执行 initialValue 方法,但如果在此之前已使用 set 方法设置值,则不再执行 initialValue 方法。
- 每个线程最多只需调用一次 initialValue 方法,后续调用 get 方法不会再次触发,除非先调用 remove 方法删除后再 get。
(二)set 方法
- 作用 :为当前线程设置一个值,设置后其他方法可通过 get 方法获取到该值。
- 使用场景 :当需要手动为线程设置特定值时使用,与 initialValue 方法的自动初始化形成对比。
(三)get 方法
- 作用 :获取当前线程所对应的值,若第一次调用且未设置值,则会触发 initialValue 方法进行初始化。
- 使用场景 :在需要获取线程本地存储的值时调用,贯穿 ThreadLocal 的各种使用场景。
(四)remove 方法
- 作用 :删除当前线程所保存的值。
- 使用场景及影响 :
- 当业务流程需要清空线程本地存储的值时使用,如在某个服务处理完成后不希望后续服务再获取到之前的值。
- 调用 remove 后,若后续再次调用 get 方法,在未重新 set 值的情况下,会重新触发 initialValue 方法(若已重写)或返回 null。
- 示例:在场景二的代码演示中,若 service 二调用 remove 方法清空用户信息,service 三将获取到 null,导致空指针异常;但可在 remove 后重新 set 新值供后续服务使用。
二、知识点总结
- initialValue 方法 :用于设置线程初始值,延迟加载,调用 get 方法时触发,未重写默认返回 null,每个线程最多调用一次,除非 remove 后重新 get。
- set 方法 :手动为线程设置值,设置后后续 get 可获取该值。
- get 方法 :获取线程对应的值,第一次调用若未设置则初始化。
- remove 方法 :删除线程保存的值,清空后可重新设置新值。
- 方法使用场景 :initialValue 适用于需要自动初始化值的情况;set 和 get 配合用于手动控制值的设置与获取;remove 用于清理线程本地存储,避免值在不同业务环节间的不当传递。
ThreadLocal 课程笔记
一、ThreadLocal 源码分析
(一)ThreadLocal 的结构及三个重要组件的关系
-
三个重要组件 :Thread、ThreadLocal 和 ThreadLocalMap。
- Thread :线程类,每个线程持有一个 ThreadLocalMap 类型的成员变量。
- ThreadLocal :为每个线程提供独立的变量副本,通过 ThreadLocalMap 实现线程与值的映射。
- ThreadLocalMap :存储在 Thread 类中,用于保存多个 ThreadLocal 和其对应的值,以键值对形式存储。
-
关系 :每个线程有一个 ThreadLocalMap,ThreadLocal 作为键,对应的值存储在该线程的 ThreadLocalMap 中。
(二)重要方法的源码解析
-
get 方法 :
- 首先获取当前线程的 ThreadLocalMap。
- 如果 ThreadLocalMap 为空,调用 initialValue 方法初始化值。
- 如果 ThreadLocalMap 不为空,以当前 ThreadLocal 为键调用 ThreadLocalMap 的 getEntry 方法获取值。
-
set 方法 :
- 获取当前线程的 ThreadLocalMap。
- 如果 ThreadLocalMap 为空,创建新的 ThreadLocalMap 并设置值。
- 如果 ThreadLocalMap 不为空,以当前 ThreadLocal 为键,将新值存入 ThreadLocalMap。
-
remove 方法 :获取当前线程的 ThreadLocalMap,并以当前 ThreadLocal 为键,从 ThreadLocalMap 中删除对应的键值对。
-
initialValue 方法 :默认返回 null,可重写该方法为 ThreadLocal 设置初始值,仅在第一次调用 get 方法且未设置值时触发。
(三)ThreadLocalMap 的实现细节
- 结构 :ThreadLocalMap 是一个 Entry 数组,每个 Entry 包含一个 ThreadLocal 类型的键和一个 Object 类型的值。
- 哈希冲突处理 :采用线性探测法,与 HashMap 的拉链法不同,发生冲突时继续查找下一个空位置存储。
二、ThreadLocal 使用的注意点
(一)内存泄露问题
- 原因 :ThreadLocalMap 中的键为 ThreadLocal 的弱引用,值为强引用。如果 ThreadLocal 不再被使用但线程未终止,其对应的值无法被垃圾回收,导致内存泄露。
- 解决方法 :在不再使用 ThreadLocal 时,主动调用 remove 方法删除键值对。
(二)空指针异常问题
- 原因 :在未设置初始值或未调用 set 方法的情况下直接调用 get 方法,若 ThreadLocal 的泛型为包装类型且涉及装箱拆箱操作,可能会导致空指针异常。
- 解决方法 :在使用 ThreadLocal 时,确保正确设置初始值或在使用前调用 set 方法。
三、知识点总结
- ThreadLocal 的结构 :由 Thread、ThreadLocal 和 ThreadLocalMap 三个组件构成,每个线程拥有自己的 ThreadLocalMap,用于存储 ThreadLocal 和对应值。
- 重要方法的源码解析 :get 方法获取值,set 方法设置值,remove 方法删除值,initialValue 方法设置初始值,ThreadLocalMap 的实现细节包括 Entry 数组结构和线性探测法处理哈希冲突。
- 使用注意点 :避免内存泄露需主动调用 remove 方法,防止空指针异常需正确设置初始值或调用 set 方法。
ThreadLocal 课程笔记
一、ThreadLocal 章节回顾
(一)两大使用场景
-
场景一:工具类线程安全问题
- 问题 :如 SimpleDateFormat 等工具类线程不安全,多线程使用时易出错。
- 解决方案 :使用 ThreadLocal 为每个线程提供独立副本,避免线程安全问题。
- 示例 :从两个线程扩展到 1000 个线程,使用线程池后,通过 ThreadLocal 解决静态实例带来的线程安全问题,无需同步,提高效率。
-
场景二:方法间传递公用信息
- 问题 :用户信息等在多个方法间传递,层层传参繁琐且易出错。
- 解决方案 :使用 ThreadLocal 在线程内保存全局信息,各方法可直接获取,无需传参。
- 示例 :通过 ServiceOne 设置用户信息,ServiceTwo 和 ServiceThree 直接获取,避免传参。
(二)两种场景对应的用法
- 场景一 :需实现 initialValue 方法,适用于对象初始化可控场景。
- 场景二 :使用 set 方法,适用于对象初始化时间不受控制场景(如拦截器),无需重写 initialValue 方法。
(三)ThreadLocal 的好处
- 线程安全 :每个线程拥有独立副本,无需同步。
- 高效 :避免加锁带来的性能损耗。
- 节省资源 :无需创建大量对象,节省内存和开销。
- 便捷 :随时随地获取之前保存的对象,免去传参麻烦。
(四)重要方法解析
- initialValue 方法 :设置初始值,延迟加载,仅在第一次调用 get 且未设置值时触发,通常只调用一次,不重写默认返回 null。
- set 方法 :设置新值,与 initialValue 二选一。
- get 方法 :获取之前设置的值,若未设置则触发 initialValue(若重写)。
- remove 方法 :删除对应值,仅删除当前 ThreadLocal 的键值对,不影响其他。
(五)源码分析
- 关键组件关系 :Thread(线程)、ThreadLocal 和 ThreadLocalMap 三者关系,每个线程持有一个 ThreadLocalMap,用于存储多个 ThreadLocal 和对应值。
- 方法实现原理 :get、set、remove 方法均通过操作 ThreadLocalMap 实现,ThreadLocalMap 使用 Entry 数组存储键值对,处理哈希冲突采用线性探测法。
(六)使用注意点
- 内存泄露 :因 ThreadLocalMap 中键为弱引用,值为强引用,线程不终止时可能导致内存泄露,需主动调用 remove 方法。
- 空指针异常 :未设置初始值或未调用 set,直接 get 时若涉及装箱拆箱操作,可能抛出空指针异常,应正确设置初始值或调用 set 方法。
二、知识点总结
- 两大使用场景 :解决工具类线程安全问题和方法间传递公用信息,分别对应不同用法。
- 重要方法 :initialValue、set、get 和 remove,理解其功能和触发条件。
- 源码结构 :Thread、ThreadLocal 和 ThreadLocalMap 的关系及方法实现原理。
- 使用注意点 :避免内存泄露和空指针异常,正确使用 ThreadLocal。