天天看点

【06期】面试官:说一下ThreadLocal和内存泄漏的问题

作者:Java面试技术栈

什么是ThreadLocal

ThreadLocal是Java中的一个类,它提供了一种将对象与线程关联的机制。每个线程都可以通过ThreadLocal获取自己的独立副本,以保证线程间的数据隔离和线程安全。

ThreadLocal的作用主要有以下几个方面:

  1. 线程隔离:每个线程可以独立地操作自己的副本,互不干扰。
  2. 线程封闭:通过ThreadLocal,可以将可变的数据封装在每个线程内部,使其具有线程级别的封闭性。
  3. 上下文传递:ThreadLocal常被用于存储线程相关的上下文信息,如用户身份、请求参数等。
  4. 避免传参:有些情况下,某些数据需要在多个方法之间传递,使用ThreadLocal可以避免频繁的参数传递。

ThreadLocal是如何工作的?

ThreadLocal核心逻辑都是在Thread类的静态内部类ThreadLocalMap中,接下来一起分析下ThreadLocalMap源码,逐个方法解析:

ThreadLocalMap类中有个静态内部类Entry,Entry可以理解为存储数据的地方。源码如下:

static class Entry extends WeakReference<ThreadLocal<?>> {
  /**
   * The value associated with this ThreadLocal.
   */
  Object value;

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

通过上边源码发现Entry竟然继承WeakReference,而且ThreadLocal竟然是个弱引用。

弱引用:弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了「只具有弱引用」的对象,不管当前内存空间足够与否,都会回收它的内存

也就是说ThreadLocal随时可能消失,也就是entry.get()返回的ThreadLocal随时可能为空。

//这个线程的关于所有的ThreadLocal保存的数据都在这,通过ThreadLocal确定下坐标找到对应的entry
private Entry[] table;
private void set(ThreadLocal<?> key, Object value) {
  
        Entry[] tab = table;
        int len = tab.length;
     //通过threadLocal确定下坐标找到对应的entry
        int i = key.threadLocalHashCode & (len-1);

    //遍历数组
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
   
            if (k == key) {
                e.value = value;
                return;
            }
   //这里!!!如果e不是null,但是e.get()是null,表示那个弱引用被垃圾回收了
            if (k == null) {
                //这个方法会删除所有过期的条目
                //意思就是将被垃圾回收掉的threadLocal,从数据table中删除
                //并且新new 一个entry代替掉它的i坐标,过分!
                replaceStaleEntry(key, value, i);
                return;
            }
        }
  //如果没有直接新增一个到数组里边
        tab[i] = new Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
    }
           

通过上边的set方法,可以发现ThreadLocalMap真正存储数据的地方是table数组里边,通过threadLocal的hash值找下坐标。如果在set的时候发现table中threadLocal被回收掉了,那么它的位置直接被顶替掉。

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}
           

这个get方法没什么好说的,就是用threadLocal的hash值去找table的下坐标,没有找到直接循环数组找。好了ThreadLocalMap的核心逻辑就分析到这。

接下来看ThreadLocal核心源码

public void set(T value) {
    // 拿到当前线程
 Thread t = Thread.currentThread();
    //获取当前线程下的ThreadLocalMap
 ThreadLocalMap map = getMap(t);
    
 if (map != null)
  map.set(this, value);
 else
  createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
 return t.threadLocals;
}

public T get() {
    // 拿到当前线程
 Thread t = Thread.currentThread();
     //获取当前线程下的ThreadLocalMap
 ThreadLocalMap map = getMap(t);
 if (map != null) {
        //通过threadLocal获取table里边的value值
  ThreadLocalMap.Entry e = map.getEntry(this);
  if (e != null) {
   @SuppressWarnings("unchecked")
   T result = (T) e.value;
   return result;
  }
 }
 return setInitialValue();
}
           
【06期】面试官:说一下ThreadLocal和内存泄漏的问题

threadLocal和ThreadMap的关系

图解释:

建了三个ThreadLocal,有两个线程,在线程里边ThreadLocalMap类的属性table分别保存着threadLocal对应的value值。

ThreadLocal内存泄漏问题

内存泄漏:是指程序在申请内存后,无法释放已申请的内存空间,导致系统无法及时回收内存并且分配给其他进程使用。通常少次数的内存无法及时回收并不会到程序造成什么影响,但是如果在内存本身就比较少获取多次导致内存无法正常回收时,就会导致内存不够用,最终导致内存溢出。

首先「说一下为什么在ThreadLocalMap中ThreadLocal是弱引用?」

如果在ThreadLocalMap中使用强引用来存储ThreadLocal对象,那么ThreadLocal对象会一直存在于内存中,即使在实际的应用中已经不再需要该ThreadLocal对象。这是因为ThreadLocalMap是与线程相关联的,保存在ThreadLocalMap的table数组中,ThreadLocalMap中的键值对不会被自动清理,而是会一直保留,从而造成内存泄漏。也就是说ThreadLocal永远不会被清理,除非手动清理,ThreadLocalMap调用set(),get(),remove()方法的时候会被清除value值。

为了解决这个问题,ThreadLocal被存储为弱引用。弱引用的特点是,当一个对象只被弱引用所引用时,在垃圾回收时会被自动回收。当ThreadLocal对象被垃圾回收时,对应的键值对也会被自动从ThreadLocalMap中移除。

「threadLocal内存泄漏问题」

如果一个线程长时间运行,一直持有ThreadLocal对象的键值,由于value被强制引用,没有及时清理导致内存泄漏。

为了避免ThreadLocal内存泄漏问题,需要进行适当的清理操作。可以使用ThreadLocal类提供的remove()方法手动清理ThreadLocal对象关联的值。

继续阅读