多线程访问共享可变数据时,涉及到线程间的数据同步的问题,并不是所有时候都需要共享数据,因此,线程封闭的概念就出来了。

线程封闭:数据被封闭在各自的线程中,不需要同步,这种通过将数据封闭在线程中而避免使用同步的技术称为线程封闭,线程封闭具体的实现有ThreadLocal、局部变量

局部变量:局部变量的固有属性就是封闭在线程中,位于执行线程的栈中,其他线程无法访问

ThreadLocal 使用

使用场景:

  1. 每个线程都需要一个独享的对象(通常是工具类,典型需要使用的类有 SimpleDateFormat 和 Random)
  2. 每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦

ThreadLocal 的作用:

  • ThreadLocal 可以让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象)
  • 在任何方法中都可以轻松的获取到该对象

根据共享对象的生成时机不同,选择 initialValue 或 set 来保存对象:

  • initialValue:在 ThreadLocal 第一次 get 的时候就可以把对象给初始化出来,即对象的初始化时机可以由我们控制,如 SimpleDateFormatter
  • set:需要保存到 ThreadLocal 中的对象生成时机不由我们随意控制,如拦截器生成的用户信息,用 ThreadLocal.set 直接放入到 ThreadLocal 中去,以便后续使用

ThreadLocal 的优势:

  1. 实现线程安全
  2. 不需要加锁,提高执行效率
  3. 更加高效的利用内存、节省开销
  4. 避免传参的麻烦,降低耦合度

场景一

场景一:每个线程需要一个独享的对象,每个 Thread 内有自己的实例副本,不共享,如 SimpleDateFormat 的使用,使用 ThreadLocal.withInitial(Suppiles) 方法进行初始化

  1. public class ThreadLocalUsageOne {
  2. public static String date(int seconds) {
  3. Date date = new Date(1000 * seconds);
  4. // 每个线程都有自己唯一的一个SimpleDateFormatter
  5. return ThreadSafeFormatter.dateFormatThreadLocal.get().format(date);
  6. }
  7. public static void main(String[] args) {
  8. ExecutorService service = Executors.newFixedThreadPool(10);
  9. for (int i = 0; i < 1000; i++) {
  10. int finalI = i;
  11. service.execute(() -> {
  12. System.out.println(ThreadLocalUsageOne.date(finalI));
  13. });
  14. }
  15. service.shutdown();
  16. }
  17. }
  18. // 通过该类获取每个线程独有的SimpleDateFormatter
  19. public class ThreadSafeFormatter {
  20. public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
  21. @Override
  22. protected SimpleDateFormat initialValue() {
  23. return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  24. }
  25. };
  26. // lambda写法
  27. public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal1 = ThreadLocal.withInitial(() -> {
  28. return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  29. });
  30. }

场景二

场景二:当前用户信息需要被线程内所有方法共享,这些信息存在如下特点:

  • 在同一线程内,当前用户信息(如用户名、user ID一致)是相同的,需要被该线程内的所有方法共享
  • 不同的业务线程中的用户信息是不同的

在该场景下,常用的解决方法:
1、一种解决方法是将 user 作为参数层层传递,从 service-1 一直传递下去,该方案会导致代码冗余和不易维护
未命名绘图.png

2、另一种解决思路是在每个线程内保存全局变量,可以在不同的方法直接使用,避免参数传递的麻烦,在这种思路,可以有如下两种实现

  • 使用一个 UserMap,缺点是多线程同时工作时,为了保证线程安全,需要使用 synchronized 或者 ConcurrentHashMap,会影响性能

image.png

  • 使用 ThreadLocal ,通过 ThreadLocal.set() 与 ThreadLocal.get() 在同一线程内存取 ```java public class ThreadLocalUsageTwo { public static void main(String[] args) {
    1. ExecutorService executorService = Executors.newFixedThreadPool(10);
    2. for (int i = 0; i < 1000; i++) {
    3. int finalI = i;
    4. executorService.execute(()->{
    5. String name = Thread.currentThread().getName()+ "-" + finalI;
    6. new Service1().process(name);
    7. });
    8. }
    9. executorService.shutdown();
    } }

class User { private String name;

  1. public User setName(String name) {
  2. this.name = name;
  3. return this;
  4. }
  5. @Override
  6. public String toString() {
  7. return "User{" +
  8. "name='" + name + '\'' +
  9. '}';
  10. }

}

// 用于生成用户类 class Service1 { public void process(String name) { // 创建用户类 User user = new User().setName(name); // 将用户类放入ThreadLocal UserContextHolder.holder.set(user); new Service2().process(); } }

// 用于持有用户类 class UserContextHolder { public static ThreadLocal holder = new ThreadLocal<>(); }

// 在其他服务中使用 class Service2 { public void process() { User user = UserContextHolder.holder.get(); System.out.println(“当前线程:” + Thread.currentThread().getName() + “\t” + user); } }

  1. <a name="LC50j"></a>
  2. #### ThreadLocal 原理
  3. Thread、ThreadLocal、ThreadLocalMap 的关系:
  4. - Thread 类中存在一个成员变量 **ThreadLocalMap** ,用于存储 ThreadLocal
  5. - 每一个 ThreadLocalMap 中,存在许多的 ThreadLocal
  6. ![屏幕截图 2020-11-17 184018.png](https://cdn.nlark.com/yuque/0/2020/png/750131/1605609737393-b6566dbe-251a-494f-96db-7463ae15c088.png#align=left&display=inline&height=847&margin=%5Bobject%20Object%5D&name=%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE%202020-11-17%20184018.png&originHeight=847&originWidth=953&size=211398&status=done&style=none&width=953)
  7. ThreadLocal 的重要方法:
  8. - T initialValue():初始化
  9. - 该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用 get() 方法的时候,才会被触发
  10. - 当线程第一次使用 get 方法访问变量时,将会调用此方法,除非线程先前调用了 set 方法,在这种情况下,不会为线程调用 initialValue 方法
  11. - 每个线程最多调用一次此方法,但如果已经调用了 remove 后,再调用 get,则可以再次调用此方法
  12. - 如果不重写此方法,这个方法会返回 null,因此,一般使用匿名内部类的方法来重写 initialValue 方法,以便在后续使用中可以初始化副本对象
  13. - void set(T t):为这个线程设置一个新值
  14. - T get():得到这个线程对应的value,如果首次调用 get,则会调用 initialize 来得到这个值
  15. - void remove():删除对应这个线程的值
  16. set 和 get 方法源码解析:
  17. - get:先取出当前线程的 ThreadLocalMap,然后调用 map.getEntry 方法,将本 ThreadLocal 的引用作为参数传入,取出 map 中属于本 ThreadLocal 的 value
  18. - 注意:这个map及map中的key和value都是保存在线程中的,而不是保存在ThreadLocal中
  19. ```java
  20. public T get() {
  21. Thread t = Thread.currentThread();
  22. ThreadLocalMap map = getMap(t);
  23. if (map != null) {
  24. ThreadLocalMap.Entry e = map.getEntry(this);
  25. if (e != null) {
  26. @SuppressWarnings("unchecked")
  27. T result = (T)e.value;
  28. return result;
  29. }
  30. }
  31. return setInitialValue();
  32. }

ThreadLocalMap 类,也就是 Thread.threadLocals,ThreadLocalMap 类是每个线程 Thread 类中的变量,里面最重要的是一个键值对数组 Entry[] table,其中

  • 键:这个 ThreadLocal
  • 值:实际需要的成员变量,如 user 或 simpleDateFormatter 对象

ThreadLocal 可能存在的问题

内存泄漏:某个对象不再有用,但占用的内存却不能被回收

弱引用:如果这个对象只被弱引用关联(没有被任何强引用关联),那么这个对象就可以被回收,即弱引用不会阻止GC

ThreadLocal 可能的内存泄露:ThreadLocalMap 中的 key 和 value
image.png

  • key:ThreadLocalMap 的每个 Entry 都是一个对 key 的弱引用,当 key 不再使用时,该引用不会阻止 key 的回收,因此 key 不会造成内存泄漏
  • value:ThreadLocalMap 的每个 Entry 都包含一个对 value 的强引用
    • 正常情况下,当线程终止,保存在 ThreadLocal 里的 value 会被因为没有任何强引用而被垃圾回收
    • 当线程不终止(即线程需要保持很久,如使用线程池),那么 key 对应的 value 就不能回收,因为有如下的调用链:Thread -> ThreadLocalMap -> Entry (key 为 null) -> value,即 value 和 Thread 之间存在强引用链路,导致 value 无法回收
    • 针对上述情况,jdk 已经做了处理,即在 set、remove、rehash 这些常用方法中会扫描 key 为 null 的 Entry,并把对应的 value 设置为 null,这样 value 对象就可以被回收
    • 然而如果一个 ThreadLocal 不被使用,那么 set、remove、rehash 方法也不会被调用,如果线程又不停止,那么调用链路就会一直存在,导致 value 的内存泄露,因此在使用完 ThreadLocal 后,一定要调用 remove 方法,删除对应的 Entry 对象,避免内存泄漏

image.png

其他 ThreadLocal 的使用问题:

  1. 空指针异常:ThreadLocal 在 get 之前不需要先进行 set,因为直接 get 会返回 null,而不会抛出空指针异常
  2. 共享对象:如果每个线程中 ThreadLocal.set 进去的东西本来就是多线程共享的一个对象,比如 static 对象,那么多个线程的 ThreadLocal.get() 取得的还是这个共享对象本身,还是存在并发访问控制问题,因此不要往 ThreadLocal 中存入共享变量
  3. 当线程数很少的时候,如果在局部变量中新建对象可以解决问题,就不需要强行使用 ThreadLocal
  4. 优先使用框架的支持,而不是自己创造:如在 spring 中,如果可以使用 RequestContextHolder,那就不需要自己维护 ThreadLocal,免得忘了调用 remove 而导致内存泄漏

Spring 中 ThreadLocal 的使用

  • DateTimeContextHolder 类
  • RequestContextHolder 类
  • 每次 HTTP 请求都对应一个线程,线程之间相互隔离,这就是 ThreadLocal 的典型应用