美文网首页
15.ThreadLocal线程持有对象

15.ThreadLocal线程持有对象

作者: 哈哈大圣 | 来源:发表于2021-12-04 01:15 被阅读0次

    一、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则是线程独享的对象

    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方法的用户做清理工作…

    相关文章

      网友评论

          本文标题:15.ThreadLocal线程持有对象

          本文链接:https://www.haomeiwen.com/subject/gethvktx.html