[toc]
ThreadLocal 两大使用场景及用法
典型场景1:每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random )
每个Thread内有自己的实例副本,不共享
比喻:教材只有一本,一起做笔记有线程安全问题。复印后没问题
- SimpleDateFormat的进化之路
- 只有两个线程分别用自己的SimpleDateFormat
- 多个线程共用一个日期格式化器
同样的日期格式化器创建了许多
-
每个线程共用一个日期格式化器
面临问题:线程安全问题,所有线程共用同一个日期格式化器
- 问题解决
- 格式化方式使用 synchronized 同一时刻只有一个线程能调用格式化日期的方法
- 更好的解决方案是使用 ThreadLocal
- 总结进化之路
- 两个线程分别用自己的SimpleDateFormat,这没问题
- 后来延伸出10个,那就有10个线程和10个SimpleDateFormat,这虽然写法不优雅(应该复用对象),但勉强可以接受
- 但是当需求变成了1000个,那么必然要用线程池(否则消耗内存太多)
- 面临问题:所有的线程都共用同一个simpleDateFormat对象,这是线程不安全的,出现了并发安全问题
- 我们可以选择加锁,加锁后结果正常,但是效率低
- 在这里更好的解决方案是使用ThreadLocal
- lambda表达式
- 代码示例
- 使用线程池方式:使用同一个日期格式化工具
/**
* @author jichao
* @version V1.0
* @description: 多个任务执行日期格式化操作
* @date 2020/09/16
*/
public class ThreadLocalNormalUse {
/**
* 1. 如果每次调用转换方法都创建一个工具类,会增加内存消耗
* 2. 所有线程共用一份日期格式化工具类,会出现线程安全问题
*/
public static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 1000; i++) {
int finalI = i;
executorService.execute(()->{
String time = new ThreadLocalNormalUse().convertDate(finalI);
System.out.println(time);
});
}
executorService.shutdown();
}
/**
* 日期格式化
* @param time
* @return
*/
private String convertDate(int time) {
Date date = new Date(time * 1000);
return simpleDateFormat.format(date);
}
}
- 加锁解决线程安全问题
/**
* @author jichao
* @version V1.0
* @description: 加锁方式
* @date 2020/09/16
*/
public class ThreadLocalNormalUse2 {
/**
* 1. 如果每次调用转换方法都创建一个工具类,会增加内存消耗
* 2. 所有线程共用一份日期格式化工具类,会出现线程安全问题
*/
public static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start("format date");
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10000; i++) {
int finalI = i;
executorService.execute(()->{
String time = new ThreadLocalNormalUse2().convertDate(finalI);
System.out.println(time);
});
}
executorService.shutdown();
while (true) {
if (executorService.isTerminated()) {
stopWatch.stop();
System.out.println(stopWatch.prettyPrint());
break;
}
}
}
/**
* 日期格式化
* @param time
* @return
*/
private String convertDate(int time) {
Date date = new Date(time * 1000);
String timeRet = null;
synchronized (ThreadLocalNormalUse2.class) {
timeRet = simpleDateFormat.format(date);
}
return timeRet;
}
}
- 使用 ThreadLocal 方式
/**
* @author jichao
* @version V1.0
* @description: 使用ThreadLocal 方式 利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存
* @date 2020/09/16
*/
public class ThreadLocalNormalUse3 {
public static void main(String[] args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start("format date");
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10000; i++) {
int finalI = i;
executorService.execute(()->{
String time = new ThreadLocalNormalUse3().convertDate(finalI);
System.out.println(time);
});
}
executorService.shutdown();
while (true) {
if (executorService.isTerminated()) {
stopWatch.stop();
System.out.println(stopWatch.prettyPrint());
break;
}
}
}
/**
* 日期格式化
* @param time
* @return
*/
private String convertDate(int time) {
Date date = new Date(time * 1000);
return SimpleDateFormatSafe.simpleDateFormat2.get().format(date);
}
}
class SimpleDateFormatSafe{
public static ThreadLocal<SimpleDateFormat> simpleDateFormat = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
/**
* lambda 方式
*/
public static ThreadLocal<SimpleDateFormat> simpleDateFormat2 = ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
典型场景2:每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦
-
案例
当前用户信息需要被线程内所有方法共享,一个比较繁琐的解决方案是把user作为参数层层传递,从service-1()传到service-2(),再从service-2()传到service-3(),以此类推,但是这样做会导致代码冗余且不易维护
-
实现
每个线程内需要保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦
-
实现方式1
-
在案例基础上可以演进,使用UserMap
-
当多线程同时工作时,我们需要保证线程安全,可以用synchronized,也可以用ConcurrentHashMap,但无论用什么,都会对性能有所影响
- 实现方式2
-
更好的办法是使用ThreadLocal,这样无需synchronized,可以在不影响性能的情况下,也无需层层传递参数,就可达到保存当前线程对应的用户信息的目的
-
其实就是将变量放入的本地线程中,不涉及到资源共享问题,所以解决了线程安全问题
- 思路
-
用ThreadLocal保存一些业务内容(用户权限信息、从用户系统获取到的用户名、user ID等)
-
这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的
-
在线程生命周期内,都通过这个静态ThreadLocal实例的get()方法取得自己set过的那个对象,避免了将这个对象(例如user对象)作为参数传递的麻烦
-
强调的是同一个请求内(同一个线程内)不同方法间的共享
-
不需重写initialValue()方法,但是必须手动调用set()方法
- 代码示例
public class ThreadLocalNormalUse4 {
public static void main(String[] args) {
UserSerivce userSerivce = new UserSerivce();
userSerivce.process();
}
}
class UserSerivce {
public void process() {
User user = new User();
user.setName("大圣神威");
UserHoderUtil.userContextHolder.set(user);
UserSerivce2 userSerivce2 = new UserSerivce2();
userSerivce2.process();
}
}
class UserSerivce2 {
public void process() {
User user = UserHoderUtil.userContextHolder.get();
System.out.println("UserSerivce2 ... userName = " + user.getName());
UserSerivce3 userSerivce3 = new UserSerivce3();
userSerivce3.process();
}
}
class UserSerivce3 {
public void process() {
User user = UserHoderUtil.userContextHolder.get();
System.out.println("UserSerivce3 ... userName = " + user.getName());
}
}
class UserHoderUtil {
public static ThreadLocal<User> userContextHolder = new ThreadLocal<>();
}
@Data
class User {
private String name;
}
总结
- ThreadLocal的两个作用
- 让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象)
- 在任何方法中都可以轻松获取到该对象
- 根据共享对象的生成时机不同,选择initialValueset来保存对象
场景一:initialValue
在 ThreadLocal 第一次 get 的时候把对象给初始化出来,对象的初始化时机可以由我们控制
场景二:set
如果需要保存到 ThreadLocal 里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息,用 ThreadLocal.set 直接放到我们的 ThreadLocal 中去,以便后续使用。
使用ThreadLocal带来的好处
-
达到线程安全
-
不需要加锁,提高执行效率
-
更高效地利用内存、节省开销:相比于每个任务都新建一个 SimpleDateFormat,显然用 ThreadLocal 可以节省内存和开销
-
免去传参的繁琐:无论是场景一的工具类,还是场景二的用户名,都可以在任何地方直接通过 ThreadLocal 拿到,再也不需要每次都传同样的参数。ThreadLocal 使得代码耦合度更低,更优雅
ThreadLocal原理、源码分析
- 搞清楚 Thread、ThreadLocal 以及 ThreadLocalMap 三者之间的关系
-
每个Thread对象中都持有一个ThreadLocalMap 成员变量
-
每一个 Thread 中都有一个 ThreadLocalMap,并且里面可以放入多个 ThreadLocal
- 示例图
主要方法介绍
- TinitialValue()∶初始化
- 该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get 的时候,才会触发
get 方法默认调用的就是初始化方法,如果不重写,默认返回null
-
当线程第一次使用 get 方法访问变量时,将调用此方法,除非线程先前调用了set方法,在这种情况下,不会为线程调用本 initialValue 方法
-
通常,每个线程最多调用一次此方法,但如果已经调用了 remove() 后,再调用 get(),则可以再次调用此方法
-
如果不重写本方法,这个方法会返回null。一般使用匿名内部类的方法来重写 initialValue() 方法,以便在后续使用中可以初始化副本对象。
-
void set(T t):为这个线程设置一个新值
-
T get():得到这个线程对应的 value。如果是首次调用get(),则会调用 initialize 来得到这个值
get 方法是先取出当前线程的 ThreadLocalMap,然后调用 map.getEntry 方法,把本 ThreadLocal 的引用作为参数传入,取出map中属于本 ThreadLocal 的 value
注意:这个 map 以及 map 中的 key 和 value 都是保存在线程中的,而不是保存在ThreadLocal中
-
void remove():删除对应这个线程的值
-
ThreadLocalMap 类
-
ThreadLocalMap 类,也就是Thread.threadLocals
-
ThreadLocalMap 类是每个线程 Thread 类里面的变量,里面最重要的是一个键值对数组 Entry [] table,可以认为是一个map,键值对∶
键:这个 ThreadLocal
值:实际需要的成员变量,比如 user 或者 simpleDateFormat 对象
数据结构
- HashMap
- ThreadLocalMap 这里采用的是线性探测法,也就是如果发生冲突,就继续找卞一个空位置,而不是用链表拉链
两种使用场景殊途同归
通过源码分析可以看出,setInitialValue 和直接 set 最后都是利用 map.set() 方法来设置值
也就是说,最后都会对应到 ThreadLocalMap 的一个Entry,只不过是起点和入口不一样
ThreadLocal 注意点
内存泄露
-
什么是内存泄漏:某个对象不再有用,但是占用的内存却不能被回收
-
Key 的泄漏:ThreadLocalMap 中的 Entry 继承自 WeakReference,是弱引用
弱引用的特点:如果这个对象只被弱引用关联(没有任何强引用关联),那么这个对象就可以被回收
- Value 的泄漏
-
ThreadLocalMap 的每个 Entry 都是一个对 key 的弱引用,同时,每个Entry 都包含了一个对 value 的强引用
-
正常情况下,当线程终止,保存在 ThreadLocal 里的 value 会被垃圾回收,因为没有任何强引用了,但是,如果线程不终止(比如线程需要保持很久),那么 key 对应的 value 就不能被回收,因为有以下的调用链:
Thread > ThreadLocalMap > Entry ( key为null ) > Value
因为 value 和 Thread 之间还存在这个强引用链路,所以导致 value 无法回收,就可能会出现OOM
-
JDK 已经考虑到了这个问题,所以在 set, remove,rehash 方法中会扫描 key 为 null 的 Entry,并把对应的 value 设置为 null,这样 value 对象就可以被回收
-
但是如果一个 ThreadLocal 不被使用,那么实际上 set,remove,rehash 方法也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么就导致了 value 的内存泄漏
-
如何避免内存泄露(阿里规约)
调用 remove 方法,就会删除对应的 Entry 对象,可以避免内存泄漏,所以使用完 ThreadLocal 之后,应该调用 remove 方法
空指针异常
-
代码示例TODO
在进行 get 之前,必须先 set,否则可能会报空指针异常?
由于返回类型会存在拆箱装箱的问题,在这个过程中会报空指针异常
共享对象
如果在每个线程中 ThreadLocal.set() 进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的 ThreadLocal.get() 取得的还是这个共享对象本身,还是有并发访问问题
如果可以不使用 ThreadLocal 就解决问题,那么不要强行使用
例如:在任务数很少的时候,在局部变量中可以新建对象就可以解决问题,那么就不需要使用到ThreadLocal
优先使用框架的支持,而不是自己创造
例如:在 Spring 中,如果可以使用 RequestContextHolder,那么就不需要自己维护 ThreadLocal,因为自己可能会忘记调用 remove() 方法等,造成内存泄漏
实际应用场景——在Spring中的实例分析
-
DateTimeContextHolder类,看到里面用了ThreadLocal
-
RequestContextHolder类
-
每次HTTP请求都对应一个线程,线程之间相互隔离,这就是 ThreadLocal 的典型应用场景
7.常见面试题