当前位置: 首页>后端>正文

15.ThreadLocal线程持有对象

一、ThreadLocal两大使用场景

  1. 每个线程需要一个独享的对象
  2. 每个线程内需要保存全局变量

1) 每个线程需要一个独享的对象

  1. 通常是工具类(线程不安全),典型需要使用的类比如SimpleDateFormat和Random
  2. ThreadLocal定义为静态变量
  3. 通过重写initialValue()方法在本地线程第一次获取对象时进行创建。
  4. 本地线程通过threadLocal.get()获取该对象。
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @auth Hahadasheng
 * @since 2020/10/27
 */
public class ThreadLocalExclusiveObj {

    private static final ThreadLocal<SimpleDateFormat> dateFormatLocal =
            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"));

    public static void main(String[] args) {

        ExecutorService executor = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            final int index = i;
            executor.execute(() -> {
                Date date = new Date(1000 * index);
                String format = dateFormatLocal.get().format(date);
                System.out.println(format);
            });
        }

        executor.shutdown();
    }
}
  • 拓展:时间格式化

注意,h和H代表的含义是不一样的
y = 年(yy或yyyy)
M = 月(MM)
d = 月中的天(dd)
h = 小时(0-12)(hh)
H = 小时(0-23)(HH)
m = 时分(mm)
s = 秒(ss)
S = 毫秒(SSS)
z = 时区文本(例如,太平洋标准时间…)
Z = 时区,时间偏移量(例如-0800)
以下是一些模式示例,其中包含每个模式如何格式化或期望解析日期的示例:
yyyy-MM-dd(2009-12-31)
dd-MM-YYYY(31-12-2009)
yyyy-MM-dd HH:mm:ss(2009-12-31 23:59:59)

HH:mm:ss.SSS(23:59.59.999)
yyyy-MM-dd HH:mm:ss.SSS(2009-12-31 23:59:59.999)
yyyy-MM-dd HH:mm:ss.SSS Z(2009-12-31 23:59:59.999 +0100)

2) 每个线程内需要保存全局变量

  1. 比如在拦截器中获取用户的信息,可以让不同方法直接使用,避免参数传递的麻烦。
  2. 在本地线程生命周期内,通过set/get方法设置获取线程独占变量,避免参数到处传递。
  3. 强调的是同一个请求内(同一个线程)不同方法间的共享。
  4. 不需要要重写initialValue()方法

可以利用共享的Map:使用static的ConcurrentHashMap,把当前线程的ID作为key,把user作为value来保存,这样可以做到线程间的隔离,但是依然有性能影响。使用ThreadLocak就没有性能影响,内部没有使用synchronized等同步机制,也无需层层传递参数。

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @auth Hahadasheng
 * @since 2020/10/29
 */
public class ThreadLocalShareInThread {

    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 20; i++) {
            final int index = i;
            pool.execute(() -> {
                User user = new User();
                user.setId(String.format("NO.%s", index + 1));
                user.setName(String.format("HHDS-%s", index + 1));
                user.setGender(index & 1);
                user.setAge(index + 10);

                UserHolder.holder.set(user);
                otherMethod();
            });
        }

        pool.shutdown();

    }

    public static void otherMethod() {
        System.out.println(UserHolder.holder.get());
        UserHolder.holder.remove();
    }
}

@Getter
@Setter
class User {
    private String id;
    private String name;
    private int gender;
    private int age;

    @Override
    public String toString() {
        return "{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", gender=" + gender +
                ", age=" + age +
                '}';
    }
}

class UserHolder {
    public static final ThreadLocal<User> holder = new ThreadLocal<>();
}

3) 总结

  1. 让某个需要用到的对象在线程间隔离,每个线程都有自己独立的对象
  2. 在任何方法中都能轻松获取该对象。
  3. initialValue使用场景:
    1. 在ThreadLocal第一次get的时候吧对象给初始化,对象的初始化时机可以由我们控制
  4. set:
    1. 如果需要保存到ThreadLocal里面的对象的生成时机不由我们随意控制,比如拦截器生成的用户信息,用ThreadLocal.set直接放进去即可

4) ThreadLocal带来的好处

  1. 达到线程安全
  2. 不需要加锁,提高执行效率
  3. 更高效地利用内存,节省开销(例如每个线程持有一个SimpleDateFormat)。
  4. 免去传参的繁琐

二、ThreadLocal原理

1) Thread与ThreadLocal以及ThreadLocalMap之间的关系

  1. 每个Thread实例都会有一个独立的ThreadLocalMap对象
  2. ThreadLocalMap中的Entry的key为ThreadLocal对应的引用(弱引用),value则是线程独享的对象
15.ThreadLocal线程持有对象,第1张
Thread与TL和TLM之间的关系.png

2) 重要方法

1> T initialValue():该方法会返回当前线程对应的“初始值”,延迟加载的方法,只有在调用get的时候才会触发。

  1. 当线程第一次使用get方法访问变量时,将调用此方法,除非线程先前调用了set方法,在这种相框下,不会为线程调用本initialValue方法。

get内部实现是检查对象是否为null,如果为null则执行initialValue()方法<重写该方法后执行重写的方法>,否则直接返回对象。

3.如果调用了remove()后,再调用get(),则可以再次调用initialValue()方法。

  1. initialValue()方法默认实现是直接返回一个null,如果需要独享对象,一般使用匿名内部类的方式重写该方法。
    • ThreadLocal.withInitial(() -> {... return ...})

2> void set(T t)

  1. 为这个线程设置一个新值

3> T get()

  1. 得到线程对应的value。如果是首次调用get()<之前没有调用void set(T t)>,则会调用initialValue来得到这个值。

4> void remoe()

  1. 删除线程对应的值。

3) 源码分析

  1. get方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value
  2. 注意,这个map以及map中的key和value都是保存在线程中的,而不是保存在ThreadLocal中
  3. initialValue方法:默认返回null,可以自定义实现
  4. remove方法,只删除ThreadLocalMap对应本ThreadLocal引用的Entry

4) ThreadLocalMap类

  1. 在Thread中以threadLocals作为成员变量

  2. ThreadLocalMap类是每个线程Thread类里面的变量,里面最重要的是一个键值对数组Entry[] table,可以认为是一个Map键值对

    1. 键:这个ThreadLocal
    2. 值:实际需要的成员变量
  3. ThreadLocalMap类使用上类似HashMap,但是在实现上略有不同,

    1. 并没有实现Map接口
  4. 解决冲突

    1. HashMap解决Hash冲突的思路是链表+红黑树
    2. ThreadLocalMap采用的是线性探测法:如果发生冲突,就继续找下一个空位置,而不是用拉链或者红黑树
  5. 可以当做为一个Map去理解

5) 两种使用场景殊途同归

  1. setInitialValue和直接set最后都是利用map.set()方法来设置值。最后都会对应到ThreadLocalMap的一个Entry,只不过起点和入口不一样。

三、ThreadLocal注意点

1) 内存泄露

弱引用:如果一个对象被弱引用关联(没有任务强引用),那这个对象可以被GC垃圾回收

  1. 内存泄露:某个对象不再有用,但是占用的内存却不能被回收。
  2. ThreadLocalMap中Entry的key的是弱引用,不会导致泄露问题。
  3. 每个Entry都包含一个对value的强引用。
  4. 正常情况下,当线程终止,保存在ThreadLocalMap里的key, value会被垃圾回收,没有任何强引用。
  5. 如果线程不终止(比如线程需要保持很久),key对应的value就不能被回收,存在如下调用链
    • Thread->ThreadLocalMap->Entry(key为null)->Value
    • value无法回收,就可能出现OOM
  6. JDK已经考虑到这个问题,所以在set,remove,rehash方法中会扫描key为null的Entry并发对应的value设置为null,这样value对象就可以被回收了。
  7. 如果一个ThreadLocal不被使用,那么实际上set,remove,rehash方法也不会被调用,如果同时
    线程又不停止,那么调用链就一直存在,就导致了value的内存泄露

2) 避免内存泄露(阿里规约)

  1. 使用完ThreadLocal之后主动调用remove方法,删除Entry对象,避免内存泄露。

3) ThreadLocal空指针异常

  1. 在进行get之前,必须先set,否则可能会报空指针异常?
    • 可能是写的代码缺陷:包装类拆箱导致
/**
 * @auth Hahadasheng
 * @since 2020/10/30
 */
public class ThreadLocalNPE {
    private static final ThreadLocal<Long> localId = new ThreadLocal<>();

    /**
     * 这里在没有调用initializeValue以及set的前提下直接调用get方法,
     * 似乎直接返回null,但是却报java.lang.NullPointerException
     * 是因为ThreadLocal定义的泛型为包装类的Long,在方法返回时拆箱
     * 发现是null,所以报空指针
     */
    public static long get() {
        return localId.get();
    }

    /**
     * 而这个方法则不会报错
     */
    public static Long get2() {
        return localId.get();
    }

    public static void main(String[] args) {
        System.out.println(get2());
        System.out.println(get());
    }
}

4) 共享对象

  1. 如果在每个线程中ThreadLocal.set进去的本来就是多线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get取得的还是这个共享对象本身,还是有并发问题。

如果可以不需要使用ThreadLocal,则不要进行强行使用。

5) 优先使用框架的支持,而不是自己创造

  1. 例如在Spring中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为自己可能会忘记调用remove()方法等,造成内存泄露。
  2. Spring中DateTimeContextHolder类,使用了ThreadLocal

6)关于弱引用被GC清理是否可用的疑惑解答

  1. 引用的关系是:Thread -> ThreadLocalMap -> Entity -> 弱引用ThreadLocal 和 数据,所以:

  2. 虽然是弱引用,但是只要其他地方还有普通引用,就不会被清理,会一直存在(1.一般在使用的时候都是定义为静态类属性常量... static final ThreadLocal<?> ...,为强引用,只要此类不被虚拟机卸载,则GC不会回收该对象,相关弱引用也不会被清理;2.线程执行产生的栈帧中局部变量表中可能也会存在该强引用)。

提示:GC Roots:虚拟机栈(栈帧中的本地变量表)中引用的对象;方法区中类静态属性引用的对象;方法区中常量引用的对象;本地方法栈中JNI(即一般说的native方法)中引用的对象

  1. 如果不是弱引用,而且用户已经不再持有这个ThreadLocal的引用并且没有调用remove方法,那么只要线程还在,ThreadLocal和数据就会一直被引用无法回收,就是内存泄漏了,所以这里用弱引用一定程度上是帮助忘记调用remove方法的用户做清理工作…

https://www.xamrdz.com/backend/3he1941501.html

相关文章: