最近做的一个项目中用到了ThreadLocal,在***中获取并存储访问请求的head中存储的用户的信息,以便进行方法级别的权限验证。用了感觉一知半解的,空闲了查询了许多的资料,并参考了慕课网的《玩转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()


本文整理自慕课网《玩转Java并发工具》


更多Java面试复习笔记和总结可访问我的面试复习专栏《Java面试复习笔记》,或者访问我另一篇博客《Java面试核心知识点汇总》查看目录和直达链接