最近做的一个项目中用到了ThreadLocal,在拦截器中存储访问用户的信息,以便进行方法级别的权限验证。用了感觉一知半解的,空闲了查询了许多的资料,并参考了慕课网的《玩转Java并发工具》课程,发现ThreadLocal是面试中很容易考到的并发类,于是将ThreadLocal相关的内容整理并记录于此,方便自己和同样准备找工作的同学学习。
快速到达看这里-->
典型应用场景
场景1:每个线程需要一个独享的对象
- 通常应用在线程不安全的工具类,如SimpleDateFormat,Random
- 每个Thread内有自己的实例副本,不共享
- 比喻:课本只有一本,一群人同时做笔记会发生冲突有线程安全问题。把课本复印成一人一本就没问题了
案例内容:
编写一个函数,计算1970年1.1 08:00:00 GMT后 seconds 秒后的时间,假设是1000个线程进行调用
- 方案1
public class ThreadLocalNormalUsage02 {
public static ExecutorService threadPool =
Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage02().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
//获取1970年1.1 08:00:00 GMT后 seconds 的时间
public String date(int seconds) {
//参数的单位是毫秒,从1970年1.1 08:00:00 gmt计时
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return dateFormat.format(date);
}
}
这种方案存在一个问题,每次调用都需要创建一个SimpleDateFormat 对象,消耗太大了,有没有解决的方案呢
- 方案2:
将SimpleDateFormat 对象抽出来作为静态变量
public static SimpleDateFormat dateFormat ;
//获取1970年1.1 08:00:00 GMT后 seconds 的时间
public String date(int seconds) {
//参数的单位是毫秒,从1970年1.1 08:00:00 gmt计时
Date date = new Date(1000 * seconds);
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(date);
}
新的问题又出现了,运行结果存在相同的值,发生了线程安全问题
- 方案3:
将使用SimpleDateFormat对象的代码锁起来
public String date(int seconds) {
//参数的单位是毫秒,从1970年1.1 08:00:00 gmt计时
Date date = new Date(1000 * seconds);
String s;
synchronized (ThreadLocalNormalUsage03.class){
s = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(date);
}
return s;
}
这种情况下还存在问题,在高并发下每个线程都需要排队获取,效率低,不适用
- 方案4:利用ThreadLocal再次升级实现,线程安全且能并行执行
//获取1970年1.1 08:00:00 GMT后 seconds 的时间
public String date(int seconds) {
//参数的单位是毫秒,从1970年1.1 08:00:00 gmt计时
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = ThreadStatrFormatter.dateFormatThreadLocal.get();
return dateFormat.format(date);
}
class ThreadStatrFormatter {
public static ThreadLocal<SimpleDateFormat>
dateFormatThreadLocal =
new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
}
};
}
场景2:当前用户信息需要被线程内所有方法共享
在应用开发中,有些参数需要被线程内许多方法使用,如权限管理,很多的方法都需要验证当前线程用户的身份信息
案例内容:一个系统中,user对象需要在很多server中进行使用
-
方案1
将user作为参数层层传递,从service1->service2->service3
以此类推。这样会导致代码冗余且难以维护 -
方案2
定义一个全局的static 的user,想要拿的时候直接获取。
这是一种错误的方案!!
因为我们现在的场景是多用户的系统,每个线程对应着不同的用户,每个线程的user是不同的 -
方案3
定义一个UserMap,每次访问从Map中获取用户的信息,多线程访问下加锁或者使用ConcurrentHashMap,但是对性能有影响 -
方案4
利用ThreadLocal,不需要锁,不影响性能。
强调的是同一个请求内不同方法间的共享代码演示:
/**
* 避免传递参数的麻烦
* ThreadLocalan案例2
* @author Chkl
* @create 2020/3/10
* @since 1.0.0
*/
public class ThreadLocalNormalUsage06 {
public static void main(String[] args) {
new Service1().process();
}
}
class Service1 {
public void process() {
User user = new User("张三");
UserContextHolder.holder.set(user);
new Service2().process();
}
}
class Service2 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("service2:" + user.name);
new Service3().process();
}
}
class Service3 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("service3:" + user.name);
}
}
class UserContextHolder {
public static ThreadLocal<User> holder
= new ThreadLocal<>();
}
class User {
String name;
public User(String name) {
this.name = name;
}
}
ThreadLocal的两个作用
- 让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象)
- 同一线程中,在任何方法中都可以轻松获取到该对象
两种初始化方法使用场景
-
场景1:initialValue
如果在ThreadLocal第一次get的时候把对象给初始化时使用,对象的初始化时机受控制 -
场景2:set
如果需要保存到ThreadLocal的对象的生成时机不由我们随意控制,如访问拦截器生成用户信息的情况下使用
使用ThreadLocal的好处
- 线程安全
- 不需要加锁,执行效率高
- 更高效的利用内存,节省开销
- 避免传参的繁琐操作
ThreadLocal与Thread的关系
一张图搞懂Thread,ThreadLocal,ThreadLocalMap三者的关系:
每个Thread对象都持有一个ThreadLocalMap成员变量
查看Thread的源码也可以发现确实存在这样一个变量
ThreadLocal的重要方法
-
initialValue()
- 该方法返回当前线程对应的初始值,使用了延迟加载,当调用get()方法是才会触发
- 当第一次使用get()方法时会调用此方法,如果调用前用set()方法设置了值就不会调用
- 当调用remove()方法后再次调用get()方法依然会调用initialize
- 如果不重写initialValue方法,直接调用get()会返回null
-
set() 为线程设置新的值
-
get()
- 得到线程对应的value,如果首次调用,则会调用initialize
- get方法是先取出当前线程的ThreadLocalMap,再通过map.getEntry(ThreadLocal)方法将本ThreadLocal的引用作为参数传入获取ThreadLocal的值
- ThreadlocalMap这个Map是存放在Thread中而不是ThreadLocal中
-
remove() 删除线程所保持的值
- remove()方法也是在ThreadlocalMap中进行操作,传入当前ThreadLocal对象的引用,删除m ap中的value的值
ThreadLocal注意点
- 最后一次使用之后应该手动的调用remove()方法,防止内存溢出
- 如果可以不使用ThreadLocal就解决问题,不要强行使用(如:任务数很少时)
- 优先使用框架的支持,而不是自己创造
Spring中,如果可以使用RequestContextHolder就不要用ThreadLocal
ThreadLocal 为什么会发生内存溢出?
ThreadLocal的存储实际是把当前线程作为key,存储数据当做value存储在ThreadLocalMap(内部实现为Entry)中,key使用的是弱引用,而value使用的是强引用。当任务执行结束后因为value没有回收导致数据不会被GC处理,会一直存在于线程中,积累到足够多就会发生内存溢出
如何解决内存溢出
最后一次使用之后应该手动的调用remove()方法
针对ThreadLocal 的内存溢出,也有相应的操作去解决,当调用set(),remove()方法时,会对没有使用的键值对进行处理(方便GC回收),所以操作完成后需要手动的对ThreadLocal 进行处理,调用threadLocal.remove()