研二学妹面试字节,竟倒在了ThreadLocal上,这是不要应届生还是不要女生啊?

简介: 【6月更文挑战第1天】研二学妹面试字节,竟倒在了ThreadLocal上,这是不要应届生还是不要女生啊?

一、写在开头

    今天和一个之前研二的学妹聊天,聊及她上周面试字节的情况,着实感受到了Java后端现在找工作的压力啊,记得在18,19年的时候,研究生计算机专业的学生,背背八股文找个Java开发工作毫无问题,但现在即便你是应届生,问的考题也非常的深入和细节了,只会背八股,没有一定的代码量和项目积累,根本找不到像样的工作,具体聊天内容如下:

image.png

既然大厂的面试都拷问到ThreadLocal了,那今天build哥就花点时间也来温习一下这个知识点吧,尽可能整理的细致一点!🤓🤓

二、ThreadLocal简介

2.1 ThreadLocal的作用

处理并发编程的时候,其核心问题是当多个线程去访问共享变量时,因为顺序、资源分配等原因带来了数据的不准确,我们叫这种情况为线程不安全,为了解决线程安全问题,在Java中可以采用Lock、 synchronzed关键字等方式,但这种方式对于没有持有锁的线程来说会阻塞,这样以来在时间性能上就有所损失。

为了解决这个问题,Java的lang包中诞生出了一个类,名为 ThreadLocal,见名知意,它被视为线程的“本地变量”,主要用来存储各线程的私有数据,当多个线程访问同一个ThreadLocal变量时,实际上它们访问的是各自线程本地存储的副本,而不是共享变量本身。因此,每个线程都可以独立地修改自己的副本,而不会影响到其他线程。这种以空间换时间的方式,可以大大的提升处理时间。

2.2 ThreadLocal的使用案例

上面了解了它的特性后,我们来写一个小demo感受一下ThreadLocal的使用。

public class TestService implements Runnable{
   
   
        // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
        //共享变量
        private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd"));

        public static void main(String[] args) throws InterruptedException {
   
   
            TestService obj = new TestService();
            //循环创建5个线程
            for(int i=0 ; i<5; i++){
   
   
                Thread t = new Thread(obj, ""+i);
                Thread.sleep(new Random().nextInt(1000));
                t.start();
            }
        }

        @Override
        public void run() {
   
   
            System.out.println("Thread:"+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
            try {
   
   
                Thread.sleep(new Random().nextInt(1000));
            } catch (InterruptedException e) {
   
   
                e.printStackTrace();
            }
            //formatter pattern is changed here by thread, but it won't reflect to other threads
            //设置副本的值
            formatter.set(new SimpleDateFormat());
            System.out.println("Thread:"+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
        }
}

输出:

Thread:0 default Formatter = yyyyMMdd
Thread:1 default Formatter = yyyyMMdd
Thread:2 default Formatter = yyyyMMdd
Thread:1 formatter = yy-M-d ah:mm
Thread:0 formatter = yy-M-d ah:mm
Thread:3 default Formatter = yyyyMMdd
Thread:2 formatter = yy-M-d ah:mm
Thread:3 formatter = yy-M-d ah:mm
Thread:4 default Formatter = yyyyMMdd
Thread:4 formatter = yy-M-d ah:mm

从输出中可以看出,虽然 Thread-0 已经改变了 formatter 的值,但 Thread-1 默认格式化值与初始化值相同并没有被修改,其他线程也一样,这说明每个线程获取ThreadLocal变量值的时候,确访问的时线程本地的副本值。

三、ThreadLocal的实现原理

我们从Thread源码入手,一步步的跟进,去探索ThreadLocal的实现原理。首先,在Thread的源码中,我们看到了这样的两句定义语句:

public class Thread implements Runnable {
   
   
    //......
    //与此线程有关的ThreadLocal值。由ThreadLocal类维护
    ThreadLocal.ThreadLocalMap threadLocals = null;

    //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    //......
}

threadLocals 、inheritableThreadLocals 都是ThreadLocalMap变量,而这个Map我们可以看作是ThreadLocal的定制化HashMap,用来存储线程本地变量的容器,是一个静态内部类,而这两个变量的值初始为null,只有当前线程调用 ThreadLocal 类的 set或get方法时才创建它们,那我们继续去看set/get方法。

【set方法解析】

public void set(T value) {
   
   
    //1. 获取当前线程实例对象
    Thread t = Thread.currentThread();

    //2. 通过当前线程实例获取到ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);

    if (map != null)
       //3. 如果Map不为null,则以当前ThreadLocal实例为key,值为value进行存入
       map.set(this, value);
    else
      //4.map为null,则新建ThreadLocalMap并存入value
      createMap(t, value);
}

在ThreadLocal的set方法中通过getMap()方法去获取当前线程的ThreadLocalMap对象,并对获取到的map进行判断,我们跟如到getMap方法中去,发现其实里面返回的是初始化定义的threadLocals变量。

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

在threadLocals没有被调用初始化方法重新赋值的时候,它为null(不为null时,直接set进行赋值,当前ThreadLocal实例为key,值为value),set方法中会去调用createMap(t,value)进行处理,我们继续跟入这个方法的源码去看看:

void createMap(Thread t, T firstValue) {
   
   
  t.threadLocals = new ThreadLocalMap(this, firstValue);
}

我们可以看到,在这个方法内部,会去新构造一个ThreadLocalMap的实例,并将value值初始化进去,并赋给threadLocals。

看完了set方法的底层实现我们知道:

  1. 最终变量存储的位置在ThreadLocalMap里,ThreadLocal可以视为这个Map的封装;
  2. 无论如何最终threadLocals存储的数据都是以线程为key,对应的局部变量为值得映射表;
  3. 因为映射表的原因,确保了每个线程的局部变量都时独立的。

【get方法解析】

看完了set的源码,我们继续来看看get方法的底层实现吧,既然有存(set)就有取(get),get 方法提供的就是获取当前线程中 ThreadLocal 的变量值的功能!

public T get() {
   
   
  //1. 获取当前线程的实例对象
  Thread t = Thread.currentThread();

  //2. 获取当前线程的ThreadLocalMap
  ThreadLocalMap map = getMap(t);
  if (map != null) {
   
   
    //3. 获取map中当前ThreadLocal实例为key的值的entry
    ThreadLocalMap.Entry e = map.getEntry(this);

    if (e != null) {
   
   
      @SuppressWarnings("unchecked")
      //4. 当前entitiy不为null的话,就返回相应的值value
      T result = (T)e.value;
      return result;
    }
  }
  //5. 若map为null或者entry为null的话通过该方法初始化,并返回该方法返回的value
  return setInitialValue();
}

我们上面提到了线程的变量值是和线程的ThreadLocal有映射关系的,所以这里将当前线程的ThreadLocal作为key去map中获取值,若map为null或者entry为null的话通过该方法初始化,并返回该方法返回的value,我们去看看setInitialValue的实现:

private T setInitialValue() {
   
   
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}
protected T initialValue() {
   
   
    return null;
}

这个方法里的实现和set几乎一模一样,这里调用了一个protected访问修饰符的方法initialValue(),这个方法可以被子类重写。

我们在2.2使用案例中写道的ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd"));这是在Java8中的写法,等价于:

private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){
   
   
    @Override
    protected SimpleDateFormat initialValue(){
   
   
        return new SimpleDateFormat("yyyyMMdd");
    }
};

setInitialValue 方法的目的是确保每个线程在第一次尝试访问其 ThreadLocal 变量时都有一个合适的值。

3.1 ThreadLocalMap

上面我们也说了,ThreadLocalMap是ThreadLocal的静态内部类,而每个线程独立的变量副本存储也是在这个Map中,它是一个定制的哈希表,底层维护了一个Entry 类型的数组类型的数组 table,它的内部提供了set、remove、getEntry等方法。

Entry静态内部类
这个Entry又是ThreadLocalMap的一个静态内部类,我们看一下它的源码:

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>,它的 value 字段用于存储与特定 ThreadLocal 对象关联的值,key 为弱引用,意味着当 ThreadLocal 外部强引用被置为 null(ThreadLocalInstance=null)时,根据可达性分析,ThreadLocal 实例此时没有任何一条链路引用它,所以系统 GC 的时候 ThreadLocal 会被回收。这种操作看似利用垃圾回收器节省了内存空间,实则存在一个风险,也就是我们下面要说的内存泄露问题!

只具有弱引用的对象,拥有更为短暂的生命周期,在GC线程扫描到它所在的内存区域的时候,一旦发现了只有弱引用的对象的时候,不管内存够不够用都会将其回收掉

四、ThreadLocal内存泄漏问题

4.1 内存泄漏的原因

如果非要问ThreadLocal有什么缺点的话,那就是使用不当的时候,会带来内存泄漏问题。

内存泄漏 是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

根据3.1中的分析,我们知道ThreadLocalMap中的使用的key是ThreadLocal的弱引用,Value为强引用,如果ThreadLocal没有被强引用的话,key会被GC掉,而value依旧存在,若我们采用任何措施的前提下,线程一直运行,那这些value值就会一直存在,过多的占用内存,导致内存泄漏!

4.2 如何解决内存泄漏

如何解决内存泄漏呢,只需要记得在使用完 ThreadLocal 中存储的内容后将它 remove 掉就可以了。

//ThreadLocal提供的清理方法
public void remove() {
   
   
    //1. 获取当前线程的ThreadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
        //2. 从map中删除以当前ThreadLocal实例为key的键值对
        m.remove(this);
}
/**
 * ThreadLocalMap中的remove方法
 */
private void remove(ThreadLocal<?> key) {
   
   
    Entry[] tab = table;
    int len = tab.length;
    int i = key.ThreadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
   
   
        if (e.get() == key) {
   
   
            //将entry的key置为null
            e.clear();
            //将该entry的value也置为null
            expungeStaleEntry(i);
            return;
        }
    }
}

除此之外,我们还可以使用Java 8引入的InheritableThreadLocal来替代ThreadLocal,它可以在子线程中自动继承父线程的线程局部变量值,从而避免在创建新线程时重复设置值的问题。但是同样需要注意及时清理资源以避免内存泄漏。

五、线程间局部变量传值问题

上面我们提到的Java8中引入的InheritableThreadLocal类,这是实现父子线程间局部变量传值的关键!
InheritableThreadLocal存在于java.lang包中是ThreadLocal的扩展,它有一个特性,那就是当创建一个新的线程时,如果父线程中有一个 InheritableThreadLocal 变量,那么子线程将会继承这个变量的值。这意味着子线程可以访问其父线程为此类变量设置的值。我们写一个小demo感受一下!

public class TestService{
   
   
    // 创建一个 InheritableThreadLocal 变量
    private static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {
   
   
        // 在主线程中设置值
        inheritableThreadLocal.set("这是父线程的值");

        System.out.println("父线程中的值: " + inheritableThreadLocal.get());

        // 创建一个子线程
        Thread childThread = new Thread(() -> {
   
   
            // 在子线程中尝试获取值,由于使用了 InheritableThreadLocal,这里会获取到父线程中设置的值
            System.out.println("子线程中的值: " + inheritableThreadLocal.get());
        });

        // 启动子线程
        childThread.start();

        // 等待子线程执行完成
        try {
   
   
            childThread.join();
        } catch (InterruptedException e) {
   
   
            e.printStackTrace();
        }
        // 主线程结束时清除值,防止潜在的内存泄漏
        inheritableThreadLocal.remove();
    }
}

输出:

父线程中的值: 这是父线程的值
子线程中的值: 这是父线程的值

输出不出所料,在子线程中获取的其实是父线程设置的inheritableThreadLocal值。

5.1 父子线程局部变量传值的实现原理

我们看到上面的输出后,应该思考这样的一个问题:子线程是怎么拿到父线程的inheritableThreadLocal值得呢?其实要从子线程的初始化开始说起,在线程Thread的内部,有着这样的一个初始化方法:

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  // 该参数一般默认是 true
                  boolean inheritThreadLocals) {
   
   
  // 省略大部分代码
  Thread parent = currentThread();

  // 复制父线程的 inheritableThreadLocals 属性,实现父子线程局部变量共享
  if (inheritThreadLocals && parent.inheritableThreadLocals != null) {
   
   
       this.inheritableThreadLocals =
    ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); 
  }
    // 省略部分代码
}

在这里将父线程的inheritableThreadLocals赋值了进来,我们跟入createInheritedMap方法中继续解析:

// 返回一个ThreadLocalMap,传值为父线程的
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
   
   
  return new ThreadLocalMap(parentMap);
}
//ThreadLoaclMap构建的过程中会调用该构造方法
private ThreadLocalMap(ThreadLocalMap parentMap) {
   
   
  Entry[] parentTable = parentMap.table;
  int len = parentTable.length;
  setThreshold(len);
  table = new Entry[len];
    // 一个个复制父线程 ThreadLocalMap 中的数据
  for (int j = 0; j < len; j++) {
   
   
    Entry e = parentTable[j];
    if (e != null) {
   
   
      @SuppressWarnings("unchecked")
      ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
      if (key != null) {
   
   
        // childValue 方法调用的是 InheritableThreadLocal#childValue(T parentValue)
        Object value = key.childValue(e.value);
        Entry c = new Entry(key, value);
        int h = key.threadLocalHashCode & (len - 1);
        while (table[h] != null)
          h = nextIndex(h, len);
        table[h] = c;
        size++;
      }
    }
  }
}

在这个构造方法中,我们终于看到了InheritableThreadLocal的身影,childValue()方法就是其中的一个方法,用来给子线程赋父线程的inheritableThreadLocals值;其实InheritableThreadLocal的源码非常非常的简单,大部分的实现都取自父类ThreadLocal。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
   
   

    protected T childValue(T parentValue) {
   
   
        return parentValue;
    }

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

    void createMap(Thread t, T firstValue) {
   
   
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

六、总结

OK,基于学妹在字节面试的考点,我们又梳理了一遍ThreadLocal,这个类大家还是要好好学一学的,毕竟在日后的工作中,我们肯定会使用到,譬如用它来保存用户登录信息,这样在同一个线程中的任何地方都可以获取到登录信息;用于保存事务上下文,这样在同一个线程中的任何地方都可以获取到事务上下文等等。

目录
相关文章
|
1月前
|
Arthas Kubernetes Java
字节面试:CPU被打满了,CPU100%,如何处理?
尼恩,一位拥有20多年经验的老架构师,针对近期读者在一线互联网企业面试中遇到的CPU 100%和红包架构等问题,进行了系统化梳理。文章详细解析了CPU 100%的三大类型问题(业务类、并发类、内存类)及其九种常见场景,提供了使用jstack和arthas两大工具定位问题的具体步骤,并分享了解决死锁问题的实战案例。尼恩还强调了面试时应先考虑回滚版本,再使用工具定位问题的重要性。此外,尼恩提供了丰富的技术资料,如《尼恩Java面试宝典》等,帮助读者提升技术水平,轻松应对面试挑战。
字节面试:CPU被打满了,CPU100%,如何处理?
|
1月前
|
Java API 对象存储
JVM进阶调优系列(2)字节面试:JVM内存区域怎么划分,分别有什么用?
本文详细解析了JVM类加载过程的关键步骤,包括加载验证、准备、解析和初始化等阶段,并介绍了元数据区、程序计数器、虚拟机栈、堆内存及本地方法栈的作用。通过本文,读者可以深入了解JVM的工作原理,理解类加载器的类型及其机制,并掌握类加载过程中各阶段的具体操作。
|
2月前
|
安全 Java 数据库连接
反问面试官3个ThreadLocal的问题
接下来,我想先说说ThreadLocal的用法和使用场景,然后反问面试官3个关于ThreadLocal的话题。
反问面试官3个ThreadLocal的问题
|
5月前
|
SQL 关系型数据库 MySQL
字节面试:MySQL自增ID用完会怎样?
字节面试:MySQL自增ID用完会怎样?
69 0
字节面试:MySQL自增ID用完会怎样?
|
4月前
|
存储 缓存 Java
Java面试题:解释Java中的内存屏障的作用,解释Java中的线程局部变量(ThreadLocal)的作用和使用场景,解释Java中的锁优化,并讨论乐观锁和悲观锁的区别
Java面试题:解释Java中的内存屏障的作用,解释Java中的线程局部变量(ThreadLocal)的作用和使用场景,解释Java中的锁优化,并讨论乐观锁和悲观锁的区别
51 0
|
4月前
|
并行计算 算法 安全
Java面试题:解释Java内存模型的内存屏障,并讨论其对多线程并发的影响,解释Java中的线程局部变量(ThreadLocal)的工作原理,解释Java中的ForkJoinPool的工作原理
Java面试题:解释Java内存模型的内存屏障,并讨论其对多线程并发的影响,解释Java中的线程局部变量(ThreadLocal)的工作原理,解释Java中的ForkJoinPool的工作原理
42 0
|
4月前
|
Java 数据库连接
Java面试题:Java内存模型中的happens-before关系,Java中的ThreadLocal是如何工作的?Java中的CountDownLatch和CyclicBarrier的区别?
Java面试题:Java内存模型中的happens-before关系,Java中的ThreadLocal是如何工作的?Java中的CountDownLatch和CyclicBarrier的区别?
37 0
|
4月前
|
前端开发 数据挖掘
字节面试:领域、子域、核心域、通用域和支撑域怎么划分?
领域驱动设计(DDD)通过划分业务领域和子域简化复杂性。领域是业务问题的范围,子域是更小的专业部分。核心域代表业务的核心竞争力,如电商中的商品、订单和支付;通用域提供跨领域服务,如用户管理;支撑域支持核心功能,如物流、客服和数据分析。这种划分帮助团队专注关键业务,提高开发效率和软件对业务需求的契合度。
203 0
|
5月前
|
Java 数据库 索引
技术好文共享:用商城维护类项目,讲下应届生如何在面试中介绍项目,如何准备面试
技术好文共享:用商城维护类项目,讲下应届生如何在面试中介绍项目,如何准备面试
44 0
|
5月前
|
存储 Java 数据库连接
来探讨一下最近面试问的ThreadLocal问题
来探讨一下最近面试问的ThreadLocal问题