目录
一、概述
1.1 介绍
1. 两大使用场景——ThreadLocal的用途
2. 使用ThreadLocal带来的好处
3. 主要方法介绍
4. 原理、源码分析
5. 注意点
1.2 ThreadLocal的两种典型应用场景
典型场景1:
每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有 SimpleDateFormat 和 Random )。
工具类通常是线程不安全的。多个线程共享一个静态工具类的话是有很大风险的、或者说肯定会出错的。这就需要我们使用 ThreadLocal 给每个线程都制作一个独享的对象。每个线程拥有工具类不同的实例,相互之间不影响。
典型使用场景2:
每个线程内需要保存全局变量(例如:在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦。
二、第一种应用场景
2.1 第一种典型场景
每个 Thread 内有自己的实例副本,不共享,其他线程访问不到。
比喻:教材只有一本,一起做笔记有线程安全问题。并发的读写会导致数据不一致。复印后没问题。
SimpleDateFormat的进化之路:
2个线程分别用自己的 SimpleDateFormat 。结果暂时没问题。
2.2 SimpleDateFormat的进化之路
延伸出10个,那就有10个线程和10个 SimpleDateFormat。写法不优雅。
但是当需求变成了1000个,那么必然要用线程池(否则消耗内存太多)。却衍生出一个问题,newFixedThreadPool(10) 10个线程对应1000个 SimpleDateFormat实例。
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage04().date(finalI);
System.out.println(date);
}
});
threadPool.shutdown();
把SimpleDateFormat 实例设为 static,所有的线程都共用同一个 SimpleDateFormat实例,会出现线程安全问题(并发安全问题)。
static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws InterruptedException {
// 代码体
}
加锁呢?加锁来解决线程安全问题。类锁。用了 synchronized 之后,加锁,保证同一时刻不可能超过一个线程来运行这段代码。这样的话,1000个线程要一个个排队了,会有性能问题。高并发情况下,一个个排队不符合我们的预期。
public String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
String s = null;
synchronized (ThreadLocalNormalUsage04.class) {
s = dateFormat.format(date);
}
return s;
}
2.3 利用 ThreadLocal 完美解决问题
更好的解决方案是使用 ThreadLocal。利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存。
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
class ThreadSafeFormatter {
// 常规写法
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
// lambda表达式写法
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
三、第二种应用场景
3.1 暂无
3.2 第二种典型场景 part1
实例:当前用户信息需要被线程内所有方法共享
一个比较繁琐的解决方案是把 user 作为参数层层传递,从 service-1() 传到 service-2() ,再从 service-2() 传到 service-3(),以此类推,这样做会导致代码冗余且不易维护。参数容易传错,每次都要对参数进行空判断。
希望:每个线程内需要保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦。
不适合用全局变量( static )来保存。
用 User Map:当多线程同时工作时,我们需要保证线程安全,可以用 synchronized,也可以用 ConcurrentHashMap,但无论用什么,都会对性能有所影响。
更好的办法是使用 ThreadLocal,这样无需 synchronized,可以在不影响性能的情况下,无需层层传递参数,就可以达到保存当前线程对应的用户信息目的。
方法:
用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、userID等)。
这些信息在同一个线程内相同,不同的线程使用的业务内容是不相同的。
在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(例如 user 对象)作为参数传递的麻烦
强调的是同一个请求内(同一个线程内)不同方法间的共享
不需要重写 initialValue() 方法,但是必须手动调用 set() 方法
3.3 第二种典型场景 part2
UserContextHolder.holder.set(user);
User user = UserContextHolder.holder.get();
UserContextHolder.holder.remove();
class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
四、ThreadLocal 的作用和主要方法
4.1 ThreadLocal 的两个作用和好处
1. 让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象)
2. 在任何方法中都可以轻松获取到该对象
根据共享对象的生成时机不同,选择 initialValue 或 set 来保存对象
场景一:initialValue
在 ThreadLocal 第一次 get 的时候把对象给初始化出来,对象的初始化时机可以由我们控制。
场景二:set
如果需要保存到 ThreadLocal 里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息。
用 ThreadLocal.set 直接放到我们的 ThreadLocal 中去,以便后续使用。
使用 ThreadLocal 带来的好处
达到线程安全
不需要加锁,提高执行效率
更高效的利用内存、节省开销
免去传参的繁琐—— ThreadLocal 使得代码耦合度更低,更优雅
4.2 暂无
4.3 ThreadLocal 的主要方法
T initialValue(): 初始化
该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get的时候,才会触发。
当线程第一次使用get方法访问变量时,将调用此方法。这正对应了 ThreadLocal 的两种典型用法。
每个线程最多调用一次此方法,但如果已经调用了 remove() 后,再调用get(),则可以再次调用此方法。
如果不重写本方法,这个方法会返回null。一般使用匿名内部类的方法来重写 initialValue() 方法。
void set(T t): 为这个线程设置一个新值
T get(): 得到这个线程对应的 value。如果是首次调用 get(),则会调用 initialize 来得到这个值。
void remove(): 删除对应这个线程的值
五、 ThreadLocal的原理和注意点
5.1 图解 ThreadLocal 原理
get 方法:
get方法是先取出当前线程的 ThreadLocalMap。然后调用 map.getEntry 方法,把本 ThreadLocal 的引用作为参数传入。取出map中属于本 ThreadLocal 的value。
注意:这个map以及map中的key和value都是保存在线程中的,而不是保存在 ThreadLocal 中。
set 方法( setInitialValue 方法很类似):
initialValue方法:
没有默认实现、如果要用initialValue方法 需要自己实现、通常使用匿名内部类的方式实现。
remove方法:
只会从 ThreadLocalMap 中,删掉本 ThreadLocal 的value
ThreadLocalMap 类:
ThreadLocalMap类,也就是Thread.threadLocals。ThreadLocalMap 类是每个线程 Thread 类里面的变量,里面最重要的是一个键值对数组 Entry[] table,可以认为是一个map,键值对:键:这个 ThreadLocal;值:实际需要的成员变量,比如 user 或者 simpleDateFormat 对象。
ThreadLocalMap 这里采用的是线性探测法,也就是如果发生冲突,就继续找下一个空位置,而不是用链表拉链。
两种使用场景殊途同归:
通过源码分析可以看出,setInitialValue 和直接set 最后都是利用 map.set() 方法来设置值。也就是说,最后都会对应到 ThreadLocalMap 的一个 Entry,只不过是起点和入口不一样。
5.2 暂无
5.3 ThreadLocal 的注意点
内存泄漏:
键是弱引用方式。弱引用特点:如果这个对象只被弱引用关联(没有任何强引用关联),那么这个对象就可以被回收。
值是强引用,会导致内存泄漏。
正常情况下,当线程终止,保存在 ThreadLocal 里的 value 会被垃圾回收,因为没有任何强引用了。但是,如果线程不终止(比如线程需要保持很久),那么key对应的value就不能被回收,因为有以下的调用链:
JDK 解决方式:
ThreadLocal 的 set remove reHash方法中会扫描key为null的Entry,并把对应的value设置为null。Help the GC。
如何避免内存泄漏(阿里规约)
调用 remove 方法,就会删除对应的 Entry 对象,可以避免内存泄漏,所以使用完 ThreadLocal 之后,应该调用 remove 方法。
在实际开发中,如果我们是用拦截器的方法获取到用户信息,那么同样应该用拦截器的方法 在这个线程请求退出之前,拦住他,并且把这里面所保存的 user 对象清除掉。这样一来才能非常稳妥的保证内存泄漏不发生。
空指针异常:
long(基本类型) 和 Long(包装类型) 用法区别,涉及装箱拆箱。
共享对象
如果可以不使用 ThreadLocal 就解决问题,那么不要强行使用
优先使用框架的支持,而不是自己创造
六、 课程总结
6.1 ThreadLocal 总结
1. 两大使用场景—— ThreadLocal 的用途
2. 使用 ThreadLocal 带来的好处
3. 主要方法介绍
4. 原理、源码分析
5. 注意点