设为首页 加入收藏

TOP

硬核剖析ThreadLocal源码,面试官看了直呼内行(三)
2023-07-25 21:27:38 】 浏览:75
Tags:ThreadLocal 源码
后,删除key、value,再删除Entry对象 if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } }

5. ThreadLocal使用注意事项

使用ThreadLocal结束,一定要调用remove方法,清理掉threadLocal数据。具体流程类似下面这样:

/**
 * @author 一灯架构
 * @apiNote ThreadLocal示例
 **/
public class ThreadLocalDemo {
    // 1. 创建ThreadLocal
    static ThreadLocal<User> threadLocal = new ThreadLocal<>();

    public void method() {
        try {
            User user = getUser();
            // 2. 给threadLocal赋值
            threadLocal.set(user);
            // 3. 执行其他业务逻辑
            doSomething();
        } finally {
            // 4. 清理threadLocal数据
            threadLocal.remove();
        }
    }
}

如果忘了调用remove方法,可能会导致两个严重的问题:

  1. 导致内存溢出

    如果线程的生命周期很长,一直往ThreadLocal中放数据,却没有删除,最终产生OOM

  2. 导致数据错乱

    如果使用了线程池,一个线程执行完任务后并不会被销毁,会继续执行下一个任务,导致下个任务访问到了上个任务的数据。

6. 常见面试题剖析

看完了ThreadLocal源码,再回答几道面试题,检验一下学习成果怎么样。

6.1 ThreadLocal是怎么保证数据安全性的?

ThreadLocal底层使用的ThreadLocalMap存储数据,而ThreadLocalMap是线程Thread的私有变量,不同线程之间数据隔离,所以即使ThreadLocal的set、get、remove方法没有加锁,也能保证线程安全。

image

6.2 ThreadLocal底层为什么使用数组?而不是一个对象?

因为在一个线程中可以创建多个ThreadLocal实例对象,所以要用数组存储,而不是用一个对象。

6.3 ThreadLocal是怎么解决哈希冲突的?

ThreadLocal使用的线性探测法法解决哈希冲突,线性探测法法具体赋值流程如下:

  1. 通过key的hashcode找到数组下标
  2. 如果数组下标位置是空或者等于当前ThreadLocal对象,直接覆盖值结束
  3. 如果不是空,就继续向下遍历,遍历到数组结尾后,再从头开始遍历,直到找到数组为空的位置,在此位置赋值结束

6.4 ThreadLocal为什么要用线性探测法解决哈希冲突?

我们都知道HashMap采用的是链地址法(也叫拉链法)解决哈希冲突,为什么ThreadLocal要用线性探测法解决哈希冲突?而不用链地址法呢?

我的猜想是可能是创作者偷懒、嫌麻烦,或者是ThreadLocal使用量较少,出现哈希冲突概率较低,不想那么麻烦。

使用链地址法需要引入链表和红黑树两种数据结构,实现更复杂。而线性探测法没有引入任何额外的数据结构,直接不断遍历数组。

结果就是,如果一个线程中使用很多个ThreadLocal,发生哈希冲突后,ThreadLocal的get、set性能急剧下降。

线性探测法相比链地址法优缺点都很明显:

优点: 实现简单,无需引入额外的数据结构。

缺点: 发生哈希冲突后,ThreadLocal的get、set性能急剧下降。

6.5 ThreadLocalMap的key为什么要设计成弱引用?

先说一下弱引用的特点:

弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。 不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

ThreadLocalMap的key设计成弱引用后,会不会我们正在使用,就被GC回收了?

这个是不会的,因为我们一直在强引用着ThreadLocal实例对象。

/**
 * @author 一灯架构
 * @apiNote ThreadLocal示例
 **/
public class ThreadLocalDemo {
    // 1. 创建ThreadLocal
    static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        // 2. 给ThreadLocal赋值
        threadLocal.set("关注公众号:一灯架构");
        // 3. 从ThreadLocal中取值
        String result = threadLocal.get();
        // 手动触发GC
        System.gc();
        System.out.println(result); // 输出 关注公众号:一灯架构

    }

}

由上面代码中得知,如果我们一直在使用threadLocal,触发GC后,并不会threadLocal实例对象。

ThreadLocalMap的key设计成弱引用的目的就是:

防止我们在使用完ThreadLocal后,忘了调用remove方法删除数据,导致数组中ThreadLocal数据一直不被回收。

/**
 * @author 一灯架构
 * @apiNote ThreadLocal示例
 **/
public class ThreadLocalDemo {
    // 1. 创建ThreadLocal
    static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        // 2. 给ThreadLocal赋值
        threadLocal.set("关注公众号:一灯架构");
        // 3. 使用完threadLocal,设置成null,模仿生命周期结束
        threadLocal = null;
        // 触发GC,这时候ThreadLocalMap的key就会被回收,但是value还没有被回收。
        // 只有等到下次执行get、set方法遍历数组,遍历到这个位置,才会删除这个无效的value
        System.gc();
    }

}

6.6 ThreadLocal为什么会出现内存泄漏?

ThreadLocal出现内存泄漏的原因,就是我们使用完ThreadLocal没有执行remove方法删除数据。

具体是哪些数据过多导致的内存泄漏呢?

一个是数组的Entry对象,Entry对象中key、value分别是ThreadLocal实例对象和泛型对象值。

因为我们在使用ThreadLocal的时候,总爱把ThreadLocal设置成类的静态变量,直到线程生命周期结束,ThreadLocal对象数据才会被回收。

另一个是数组中Entry对象的value值,也就是泛型对象值。虽然ThreadLocalMap的key被设置成弱引用,会被GC回收,但是value并没有被回收。需要等到

首页 上一页 1 2 3 4 下一页 尾页 3/4/4
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇爱上源码,重学Spring AOP深入 下一篇一个脚本实现 SSL 证书到期监控,..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目