阿里面试官问我ThreadLocal,我一口气给他说了四种!

简介: 阿里面试官问我ThreadLocal,我一口气给他说了四种!

什么是ThreadLocal

ThreadLocal类顾名思义可以理解为线程本地变量。也就是说如果定义了一个ThreadLocal
每个线程往这个ThreadLocal中读写是线程隔离,互相之间不会影响的。它提供了一种将可变数据通过每个线程有自己的独立副本从而实现线程封闭的机制。

实际应用

实际开发中我们真正使用ThreadLocal的场景还是比较少的,大多数使用都是在框架里面。最常见的使用场景的话就是用它来解决数据库连接、Session管理等保证每一个线程中使用的数据库连接是同一个。还有一个用的比较多的场景就是用来解决SimpleDateFormat解决线程不安全的问题,不过现在java8提供了DateTimeFormatter它是线程安全的,感兴趣的同学可以去看看。还可以利用它进行优雅的传递参数,传递参数的时候,如果父线程生成的变量或者参数直接通过ThreadLocal传递到子线程参数就会丢失,这个后面会介绍一个其他的ThreadLocal来专门解决这个问题的。

ThreadLocal api介绍

ThreadLocal的API还是比较少的就几个api
在这里插入图片描述
我们看下这几个api的使用,使用起来也超级简单

private static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(()->"java金融");
public static void main(String[] args) {
    System.out.println("获取初始值:"+threadLocal.get());
    threadLocal.set("关注:【java金融】");
    System.out.println("获取修改后的值:"+threadLocal.get());
    threadLocal.remove();
}

输出结果:

获取初始值:java金融
获取修改后的值:关注:【java金融】

是不是炒鸡简单,就几行代码就把所有api都覆盖了。下面我们就来简单看看这几个api的源码吧。

成员变量

    /**初始容量,必须为2的幂
     * The initial capacity -- MUST be a power of two.
     */
    private static final int INITIAL_CAPACITY = 16;

    /** Entry表,大小必须为2的幂
     * The table, resized as necessary.
     * table.length MUST always be a power of two.
     */
    private Entry[] table;

    /**
     * The number of entries in the table.
     */
    private int size = 0;

    /**
     * The next size value at which to resize.
     */
    private int threshold; // Default to 0

这里会有一个面试经常问到的问题:为什么entry数组的大小,以及初始容量都必须是2的幂?对于 firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 以及很多源码里面都是使用
hashCode &( $2^n$-1) 来代替hashCode% $2^n$。
这种写法好处如下:

  • 使用位运算替代取模,提升计算效率。
  • 为了使不同 hash 值发生碰撞的概率更小,尽可能促使元素在哈希表中均匀地散列。

set方法

public void set(T value) {

    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

set方法还是比较简单的,我们可以重点看下这个方法里面的ThreadLocalMap,它既然是个map(注意不要与java.util.map混为一谈,这里指的是概念上的map),肯定是有自己的key和value组成,我们根据源码可以看出它的key是其实可以把它简单看成是ThreadLocal,但是实际上ThreadLocal中存放的是ThreadLocal的弱引用,而它的value的话是我们实际set的值

static class Entry extends WeakReference> {

        /** The value associated with this ThreadLocal. */
        Object value; // 实际存放的值

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

Entry就是是ThreadLocalMap里定义的节点,它继承了WeakReference类,定义了一个类型为Objectvalue,用于存放塞到ThreadLocal里的值。我们再来看下这个ThreadLocalMap是位于哪里的?我们看到ThreadLocalMap 是位于Thread里面的一个变量,而我们的值又是放在ThreadLocalMap,这样的话我们就实现了每个线程间的隔离。下面两张图的基本就把ThreadLocal的结构给介绍清楚了。
在这里插入图片描述
此图来自网上
接下来我们再看下ThreadLocalMap里面的数据结构,我们知道HaseMap解决hash冲突是由链表和红黑树(jdk1.8)来解决的,但是这个我们看到ThreadLocalMap只有一个数组,它是怎么来解决hash冲突呢?ThreadLocalMap采用线性探测的方式,什么是线性探测呢?就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。

    /**
     * Increment i modulo len.
     */
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }

    /**
     * Decrement i modulo len.
     */
    private static int prevIndex(int i, int len) {
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }

这种方式的话如果一个线程里面有大量的ThreadLocal就会产生性能问题,因为每次都需要对这个table进行遍历,清空无效的值。所以我们在使用的时候尽可能的使用少的ThreadLocal,不要在线程里面创建大量的ThreadLocal,如果需要设置不同的参数类型我们可以通过ThreadLocal来存放一个ObjectMap这样的话,可以大大减少创建ThreadLocal的数量。
伪代码如下:

public final class HttpContext {

private HttpContext() {
}
private static final ThreadLocal<Map<String, Object>> CONTEXT = ThreadLocal.withInitial(() -> new ConcurrentHashMap(64));
public static <T> void add(String key, T value) {
    if(StringUtils.isEmpty(key) || Objects.isNull(value)) {
        throw new IllegalArgumentException("key or value is null");
    }
    CONTEXT.get().put(key, value);
}
public static <T> T get(String key) {
    return (T) get().get(key);
}
public static Map<String, Object> get() {
    return CONTEXT.get();
}
public static void remove() {
    CONTEXT.remove();
}

}

这样的话我们如果需要传递不同的参数,可以直接使用一个ThreadLocal就可以代替多个ThreadLocal了。
如果觉得不想这么玩,我就是要创建多个ThreadLocal,我的需求就是这样,而且性能还得要好,这个能不能实现列?可以使用nettyFastThreadLocal可以解决这个问题,不过要配合使FastThreadLocalThread或者它子类的线程线程效率才会更高,更多关于它的使用可以自行查阅资料哦。

下面我们先来看下它的这个哈希函数

// 生成hash code间隙为这个魔数,可以让生成出来的值或者说ThreadLocal的ID较为均匀地分布在2的幂大小的数组中。
private static final int HASH_INCREMENT = 0x61c88647;

/**
 * Returns the next hash code.
 */
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

可以看出,它是在上一个被构造出的ThreadLocalID/threadLocalHashCode的基础上加上一个魔数0x61c88647的。这个魔数的选取与斐波那契散列有关,0x61c88647对应的十进制为1640531527.当我们使用0x61c88647这个魔数累加对每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂(数组的长度)取模,得到的结果分布很均匀。我们可以来也演示下通过这个魔数

public class MagicHashCode {

private static final int HASH_INCREMENT = 0x61c88647;

public static void main(String[] args) {
    hashCode(16); //初始化16
    hashCode(32); //后续2倍扩容
    hashCode(64);
}

private static void hashCode(Integer length) {
    int hashCode = 0;
    for (int i = 0; i < length; i++) {
        hashCode = i * HASH_INCREMENT + HASH_INCREMENT;//每次递增HASH_INCREMENT
        System.out.print(hashCode & (length - 1));
        System.out.print(" ");
    }
    System.out.println();
}

}

运行结果:

7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0
7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0
7 14 21 28 35 42 49 56 63 6 13 20 27 34 41 48 55 62 5 12 19 26 33 40 47 54 61 4 11 18 25 32 39 46 53 60 3 10 17 24 31 38 45 52 59 2 9 16 23 30 37 44 51 58 1 8 15 22 29 36 43 50 57 0

不得不佩服下这个作者,通过使用了斐波那契散列法,来保证哈希表的离散度,让结果很均匀。可见代码要写的好,数学还是少不了啊。其他的源码就不分析了,大家感兴趣可以自行去查看下。

ThreadLocal的内存泄露

关于ThreadLocal是否会引起内存泄漏也是一个比较有争议性的问题。首先我们需要知道什么是内存泄露?

在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。

ThreadLocal的内存泄露情况:

  • 线程的生命周期很长,当ThreadLocal没有被外部强引用的时候就会被GC回收(给ThreadLocal置空了):ThreadLocalMap会出现一个keynullEntry,但这个Entryvalue将永远没办法被访问到(后续在也无法操作set、get等方法了)。如果当这个线程一直没有结束,那这个keynullEntry因为也存在强引用(Entry.value),而Entry被当前线程的ThreadLocalMap强引用(Entry[] table),导致这个Entry.value永远无法被GC,造成内存泄漏。
    下面我们来演示下这个场景

public static void main(String[] args) throws InterruptedException {

    ThreadLocal<Long []> threadLocal = new ThreadLocal<>();
    for (int i = 0; i < 50; i++) {
        run(threadLocal);
    }
    Thread.sleep(50000);
    // 去除强引用
    threadLocal = null;
    System.gc();
    System.runFinalization();
    System.gc();
}

private static void run(ThreadLocal<Long []> threadLocal) {
    new Thread(() -> {
        threadLocal.set(new Long[1024 * 1024 *10]);
        try {
            Thread.sleep(1000000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
}

通过jdk自带的工具jconsole.exe会发现即使执行了gc 内存也不会减少,因为key还被线程强引用着。效果图如下:
在这里插入图片描述

  • 针对于这种情况
    ThreadLocalMap在设计中,已经考虑到这种情况的发生,你只要调用了set()、get()、remove()方法都会调用cleanSomeSlots()、expungeStaleEntry()方法去清除keynullvalue。这是一种被动的清理方式,但是如果ThreadLocalset(),get(),remove()方法没有被调用,就会导致value的内存泄漏。它的文档推荐我们使用static修饰的ThreadLocal,导致ThreadLocal的生命周期和持有它的类一样长,由于ThreadLocal有强引用在,意味着这个ThreadLocal不会被GC。在这种情况下,我们如果不手动删除,Entrykey永远不为null,弱引用也就失去了意义。所以我们在使用的时候尽可能养成一个好的习惯,使用完成后手动调用下remove方法。其实实际生产环境中我们手动remove大多数情况并不是为了避免这种keynull的情况,更多的时候,是为了保证业务以及程序的正确性。比如我们下单请求后通过ThreadLocal构建了订单的上下文请求信息,然后通过线程池异步去更新用户积分,这时候如果更新完成,没有进行remove操作,即使下一次新的订单会覆盖原来的值但是也是有可能会导致业务问题。

如果不想手动清理是否还有其他方式解决下列?
FastThreadLocal 可以去了解下,它提供了自动回收机制。

  • 在线程池的场景,程序不停止,线程一直在复用的话,基本不会销毁,其实本质就跟上面例子是一样的。如果线程不复用,用完就销毁了就不会存在泄露的情况。因为线程结束的时候会jvm主动调用exit方法清理。

      /**
    • This method is called by the system to give a Thread
    • a chance to clean up before it actually exits.
      */

    private void exit() {

    if (group != null) {
        group.threadTerminated(this);
        group = null;
    }
    /* Aggressively null out all reference fields: see bug 4006245 */
    target = null;
    /* Speed the release of some of these resources */
    threadLocals = null;
    inheritableThreadLocals = null;
    inheritedAccessControlContext = null;
    blocker = null;
    uncaughtExceptionHandler = null;

    }

InheritableThreadLocal

文章开头有提到过父子之间线程的变量传递丢失的情况。但是InheritableThreadLocal提供了一种父子线程之间的数据共享机制。可以解决这个问题。

static ThreadLocal threadLocal = new ThreadLocal<>();

static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();

public static void main(String[] args) throws InterruptedException {
    threadLocal.set("threadLocal主线程的值");
    Thread.sleep(100);
    new Thread(() -> System.out.println("子线程获取threadLocal的主线程值:" + threadLocal.get())).start();
    Thread.sleep(100);
    inheritableThreadLocal.set("inheritableThreadLocal主线程的值");
    new Thread(() -> System.out.println("子线程获取inheritableThreadLocal的主线程值:" + inheritableThreadLocal.get())).start();

}

输出结果

线程获取threadLocal的主线程值:null
子线程获取inheritableThreadLocal的主线程值:inheritableThreadLocal主线程的值

但是InheritableThreadLocal和线程池使用的时候就会存在问题,因为子线程只有在线程对象创建的时候才会把父线程inheritableThreadLocals中的数据复制到自己的inheritableThreadLocals中。这样就实现了父线程和子线程的上下文传递。但是线程池的话,线程会复用,所以会存在问题。如果要解决这个问题可以有什么办法列?大家可以思考下,或者在下方留言哦。如果实在不想思考的话,可以参考下阿里巴巴的transmittable-thread-local哦。

总结

  • 大概介绍了ThreadLocal的常见用法,以及大致实现原理,以及关于ThreadLocal的内存泄露问题,以及关于使用它需要注意的事项,以及如何解决父子线程之间的传递。介绍了ThreadLocal、InheritableThreadLocal、FastThreadLocal、transmittable-thread-local各种使用场景,以及需要注意的事项。本文重点介绍了ThreadLocal,如果把这个弄清楚了,其他几种ThreadLocal就更好理解了。

结束

  • 由于自己才疏学浅,难免会有纰漏,假如你发现了错误的地方,还望留言给我指出来,我会对其加以修正。
  • 如果你觉得文章还不错,你的转发、分享、赞赏、点赞、留言就是对我最大的鼓励。
  • 感谢您的阅读,十分欢迎并感谢您的关注。

巨人的肩膀摘苹果:
https://zhuanlan.zhihu.com/p/40515974
https://www.cnblogs.com/aspirant/p/8991010.html
https://www.cnblogs.com/jiangxinlingdu/p/11123538.html
https://blog.csdn.net/hewenbo111/article/details/80487252

目录
相关文章
|
5月前
|
Python 开发工具
2024年Python最全使用Python实现音频双通道分离,2024年最新阿里p7面试难度
2024年Python最全使用Python实现音频双通道分离,2024年最新阿里p7面试难度
2024年Python最全使用Python实现音频双通道分离,2024年最新阿里p7面试难度
|
6天前
|
消息中间件 存储 canal
阿里面试:canal+MQ,会有乱序的问题吗?
本文详细探讨了在阿里面试中常见的问题——“canal+MQ,会有乱序的问题吗?”以及如何保证RocketMQ消息有序。文章首先介绍了消息有序的基本概念,包括全局有序和局部有序,并分析了RocketMQ中实现消息有序的方法。接着,针对canal+MQ的场景,讨论了如何通过配置`canal.mq.partitionsNum`和`canal.mq.partitionHash`来保证数据同步的有序性。最后,提供了多个与MQ相关的面试题及解决方案,帮助读者更好地准备面试,提升技术水平。
阿里面试:canal+MQ,会有乱序的问题吗?
|
2天前
|
消息中间件 架构师 Java
阿里面试:秒杀的分布式事务, 是如何设计的?
在40岁老架构师尼恩的读者交流群中,近期有小伙伴在面试阿里、滴滴、极兔等一线互联网企业时,遇到了许多关于分布式事务的重要面试题。为了帮助大家更好地应对这些面试题,尼恩进行了系统化的梳理,详细介绍了Seata和RocketMQ事务消息的结合,以及如何实现强弱结合型事务。文章还提供了分布式事务的标准面试答案,并推荐了《尼恩Java面试宝典PDF》等资源,帮助大家在面试中脱颖而出。
|
5天前
|
SQL 关系型数据库 MySQL
阿里面试:MYSQL 事务ACID,底层原理是什么? 具体是如何实现的?
尼恩,一位40岁的资深架构师,通过其丰富的经验和深厚的技術功底,为众多读者提供了宝贵的面试指导和技术分享。在他的读者交流群中,许多小伙伴获得了来自一线互联网企业的面试机会,并成功应对了诸如事务ACID特性实现、MVCC等相关面试题。尼恩特别整理了这些常见面试题的系统化解答,形成了《MVCC 学习圣经:一次穿透MYSQL MVCC》PDF文档,旨在帮助大家在面试中展示出扎实的技术功底,提高面试成功率。此外,他还编写了《尼恩Java面试宝典》等资料,涵盖了大量面试题和答案,帮助读者全面提升技术面试的表现。这些资料不仅内容详实,而且持续更新,是求职者备战技术面试的宝贵资源。
阿里面试:MYSQL 事务ACID,底层原理是什么? 具体是如何实现的?
|
5天前
|
Kubernetes 架构师 算法
阿里面试:全国14亿人,统计出重名最多的前100个姓名
文章介绍了如何解决“从全国14亿人的数据中统计出重名人数最多的前100位姓名”的面试题,详细分析了多种数据结构的优缺点,最终推荐使用前缀树(Trie)+小顶堆的组合。文章还提供了具体的Java代码实现,并讨论了在内存受限情况下的解决方案,强调了TOP N问题的典型解题思路。最后,鼓励读者通过系统化学习《尼恩Java面试宝典》提升面试技巧。
阿里面试:全国14亿人,统计出重名最多的前100个姓名
|
11天前
|
存储 缓存 NoSQL
阿里面试题:缓存的一些常见的坑,你遇到过哪些,怎么解决的?
阿里面试题:缓存的一些常见的坑,你遇到过哪些,怎么解决的?
|
6天前
|
存储 Kubernetes 架构师
阿里面试:JVM 锁内存 是怎么变化的? JVM 锁的膨胀过程 ?
尼恩,一位经验丰富的40岁老架构师,通过其读者交流群分享了一系列关于JVM锁的深度解析,包括偏向锁、轻量级锁、自旋锁和重量级锁的概念、内存结构变化及锁膨胀流程。这些内容不仅帮助群内的小伙伴们顺利通过了多家一线互联网企业的面试,还整理成了《尼恩Java面试宝典》等技术资料,助力更多开发者提升技术水平,实现职业逆袭。尼恩强调,掌握这些核心知识点不仅能提高面试成功率,还能在实际工作中更好地应对高并发场景下的性能优化问题。
|
1月前
|
缓存 监控 NoSQL
阿里面试让聊一聊Redis 的内存淘汰(驱逐)策略
大家好,我是 V 哥。粉丝小 A 面试阿里时被问到 Redis 的内存淘汰策略问题,特此整理了一份详细笔记供参考。Redis 的内存淘汰策略决定了在内存达到上限时如何移除数据。希望这份笔记对你有所帮助!欢迎关注“威哥爱编程”,一起学习与成长。
|
1月前
|
安全 Java 数据库连接
反问面试官3个ThreadLocal的问题
接下来,我想先说说ThreadLocal的用法和使用场景,然后反问面试官3个关于ThreadLocal的话题。
反问面试官3个ThreadLocal的问题
|
2月前
|
JavaScript
给原始数据类型加属性和方法为什么不会报错?包装类——阿里面试题
给原始数据类型加属性和方法为什么不会报错?包装类——阿里面试题