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

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方法的用户做清理工作…
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容