面试博弈之Threadlocal

本文通过一个面试场景,详细介绍了ThreadLocal的工作原理和使用,包括如何实现线程隔离、与HashMap的区别、弱引用的作用以及如何避免内存泄漏。ThreadLocal常用于存储线程局部变量,确保在多线程环境下数据的安全性。
摘要由CSDN通过智能技术生成

面试官:看你简历上写着熟悉并发编程

死牛胖子:是的(毫不客气…

面试官:有用过 Threadlocal 吗?

死牛胖子:用过(这不小KS吗,但还是要表现地谦虚一点,马上开始秀起来…

死牛胖子:Threadlocal 是一个用于保障线程安全的类,我们可以将一些数据存放到 Threadlocal 中,之后再取出来使用,Threadlocal 可以保证这些数据在不同线程间互不干扰。比如,在我们之前的项目里,会将当前用户的登录信息存放在 ThreadLocal 中,每一个请求到达后端,首先需要做登录认证,认证成功,就会将认证的用户信息存放到 ThreadLocal 中,然后请求进入到 Controller 的方法中,执行业务处理,在整个的处理过程中,我们可以随时从 ThreadLocal 中取出当前登录的用户信息来使用。ThreadLocal 可以保证每个请求对应的用户信息是当前用户,不会因为多个请求并发造成数据被修改。

面试官:能把刚刚的场景使用伪代码实现一下吗?这里有纸笔。

死牛胖子:(卧槽,还要写代码,虽然心里有点抗拒,但手已经不自觉地开始活动起来…

public class SecurityContext {

    private static ThreadLocal<AuthInfo> context = new InheritableThreadLocal<>() {

        @Override
        protected AuthInfo initialValue() {
            return new AuthInfo();
        }

    };

    public static ThreadLocal<AuthInfo> getContext() {
        return context;
    }

    public static ThreadLocal<AuthInfo> getAuthInfo() {
        return context.get();
    }
    
    public static class AuthInfo {
        // ...
    }
    
}

public class AuthFilter implements Filter {

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) {
        AuthInfo authInfo = checkAuthInfo(servletRequest);
        SecurityContext.getContext().set(authInfo);
        // ...
        filterChain.doFilter(servletRequest, servletResponse);
    }
    
}

死牛胖子:首先需要一个 ThreadLocal 的静态变量,然后声明一个 Filter,拦截所有的请求,在 Filter 中对请求中的 Token 或者 Session 进行解释,获取当前用户信息,然后把用户信息设置到 ThreadLocal 中,这样在后续的操作中就可以取出来使用了。

面试官:不错,看来用的还是挺丝滑,那么我们再深入一点,说一下 ThreadLocal 是如何实现线程隔离的

死牛胖子:嗯…(思考一下,这是要说实现原理啊,还好看过源码,还可以秀…

死牛胖子:这个问题,我们可以从源码的实现来进行分析(哥可是看过源码的淫)

首先看一下 Thread 类的实现,Thread 类中有一个 ThreadLocalMap 的成员变量,这个 MapKey 就是 ThreadLocal

public class Thread implements Runnable {
    ThreadLocalMap threadLocals = null;
}

然后看一下 ThreadLocalset() 方法的实现,首先获取当前线程的 Thread 实例,然后获取到 Thread 类中的 ThreadLocalMap 变量,然后使用 ThreadLocal 自身作为 Key 将信息存入 ThreadLocalMap。所以,我们通过 ThreadLocal 存储的信息实际上是存储在当前的线程的 Thread 实例中,而每个线程拥有自己唯一的 Thread 实例,在同一个线程内使用 Thread 实例的变量,就像使用本地变量一样,自然就实现了线程隔离。

public class ThreadLocal<T> {

    public void set(T var1) {
        Thread var2 = Thread.currentThread();
        ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
        if (var3 != null) {
            var3.set(this, var1);
        } else {
            this.createMap(var2, var1);
        }
    }
    
    public T get() {
        Thread var1 = Thread.currentThread();
        ThreadLocal.ThreadLocalMap var2 = this.getMap(var1);
        if (var2 != null) {
            ThreadLocal.ThreadLocalMap.Entry var3 = var2.getEntry(this);
            if (var3 != null) {
                Object var4 = var3.value;
                return var4;
            }
        }
        return this.setInitialValue();
    }

    public void remove() {
        ThreadLocal.ThreadLocalMap var1 = this.getMap(Thread.currentThread());
        if (var1 != null) {
            var1.remove(this);
        }
    }

    ThreadLocal.ThreadLocalMap getMap(Thread var1) {
        return var1.threadLocals;
    }
}

面试官:(又被你装到了)那这个 ThreadLocalMap 跟我们常用的 HashMap 有什么区别?

死牛胖子:这两个 Map 都是键值对的结构,但是底层实现是不一样的。HashMap 底层使用的数组 + 链表的结构实现,如果长度超过 8,则会转换为红黑树。ThreadLocalMap 则是单纯使用数组实现,另外 ThreadLocalMap 的键前面说是 ThreadLocal 实例是不严谨的,正确地说应该是 ThreadLocal 的弱引用。

面试官:你刚说 ThreadLocalMap 是使用数组实现,如果出现 hash 冲突,如何解决?

死牛胖子:HashMap 本来也是数组,之所以加上链表是为了解决 hash 冲突的问题。ThreadLocalMap 既然没有链表来进行辅助,那么只能存储在数组上,它采用了一种开放定址法。比如,在往 ThreadLocalMap 插入数据时发生冲突,也就是说数组中当前位置的值与当前的 ThreadLocal 不匹配,那么继续寻找下一个位置,如果下一个位置为空或者正好就是当前的 ThreadLocal,那么就将值设置进去,否则继续寻找下一个。所以看上去跟链表差不多,只不过是在同一个数组中通过一个查找位置的规则来实现。

面试官:不错,了解的还挺细,前面你提到 ThreadLocalMap 的键使用的是弱引用,可以细讲一下吗?

死牛胖子:我先说一下什么是弱引用吧。

面试官:好的

死牛胖子:JDK 为了更好地帮助 JAVA 虚拟机进行垃圾回收,在引用的基础上,增加了软引用,弱引用以及虚引用,原来的引用就改称为强引用。

  • 强引用:强引用的意思就是只要引用在,就不能被回收,即便此时内存空间不足,抛 OOM 异常,程序中止也不能回收,这就是强引用,确实比较强。而软引用,弱引用,虚引用则是一级级递减。
  • 软引用:如果一个对象只持有软引用,在内存空间充足时,垃圾回收器不会回收它,但是,如果内存空间不足了,这些对象就会被回收,这个常用于缓存。
  • 弱引用:如果一个对象只持有弱引用,只要进行 GC,不管当前内存空间足够与否,都会对其进行回收,所以弱引用是比较脆弱的。
  • 虚引用:如果一个对象仅持有虚引用,在任何时候都可能被回收,跟没有引用效果差不多,形同虚设。

死牛胖子:(呼…这次背的还可以

面试官:为什么 ThreadlocalMapKey 要设置成弱引用呢?

死牛胖子:为了更好地释放内存,用一个简单的示例来说明一下

public class ThreadLocalDemo {

    public void execute() {
        test();
        // ......
    }
    
    public void test() {
        ThreadLocal<Integer> intLocal = new ThreadLocal<>();
        intLocal.set(1);
    }
}

intLocal 在声明时,拥有了一个强引用,当执行 intLocal.set(1) 时,会存储到 Thread 类中的 ThreadlocalMap 变量中,并以弱引用形式存储,intLocal 又拥有了一个弱引用,此后 test() 执行完毕,intLocal 的强引用失效,只剩下一个弱引用。此时,因为强引用不存在了,Thread 无法自行删除该键值对,而弱引用则让虚拟机可以自行回收。

放到实际应用中,就更加明显的,在实际应用中一般会使用线程池技术,线程在请求完成之后并不会消亡,而是被回收至线程池中,后续请求到时,还可以拿出来继续使用,如果使用强引用的话,那么 ThreadLocal 实例就会一直存在于 Thread 对象中,只要线程不消亡,ThreadLocal 实例就不会被回收,可能导致内存泄漏。

面试官:既然 ThreadlocalMapKey 已经设置成弱引用了,是不是就杜绝了内存泄漏的风险?

死牛胖子:不然(开始装B…

死牛胖子:通过前面的分析,ThreadlocalMapKeyGC 时会被自动回收,但是 value 不会被回收啊,内存泄漏的风险依然存在。我们可以模拟一下 ThreadlocalMap 的实现

public class Test {

    public static void main(String[] args) {
        Object key = new Object();
        Object value = new Object();
        //弱引用
        Entry entryReference = new Entry(key, value);
        WeakReference<Object> valueReference = new WeakReference<>(value);
        key = null;
        value = null;
        entryReference.value = null;  // 重要一行
        System.gc();
        System.out.println(entryReference.get()); // null
        System.out.println(valueReference.get()); // null
    }

    static class Entry extends WeakReference<Object> {
        Object value;

        Entry(Object var1, Object var2) {
            super(var1);
            this.value = var2;
        }
    }

}

执行结果是打印两个 null,说明 keyvalue 都被回收的,但如果 entryReference.value = null 注释掉,再执行,则结果就不一样了,valueReference 的值依然还在。

null
java.lang.Object@133314b

死牛胖子:(神马情况,怎么不自觉地又开始写上代码了…

面试官:那怎么样才能彻底解决这个内存泄漏的问题呢?

死牛胖子:其实也简单,Threadlocal 类有提供一个 remove 方法,可以手动清理,只要在请求结束后手动清理一下,就可以防止内存泄漏了。

面试官:回答的不错,回家等消息吧,一年内我们的HR会联系你的。

死牛胖子:…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值