【八股】暑期实习八股复盘(二月)

针对一下个人面试答的不算太熟练的八股复盘一下,这帮人问的问题怎么都是这些,真是题题又库库啊。

1. Java 基础

1.1 hashCode 和 equals

重写equals()时必须同步重写hashCode(),确保两者逻辑一致(例如,基于相同属性生成哈希值)

也就是说 hashCode() 方法是专为集合类而生的。

1.2 JIT & AOT

1.2.1 JIT 即时编译

JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT(Just in Time Compilation) 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言

1.2.2 AOT 提前编译

JDK 9 引入了一种新的编译模式 AOT(Ahead of Time Compilation) 。和 JIT 不同的是,这种编译模式会在程序被执行前就将其编译成机器码,属于静态编译(C、 C++,Rust,Go 等语言就是静态编译)。AOT 避免了 JIT 预热等各方面的开销,可以提高 Java 程序的启动速度,避免预热时间长。并且,AOT 还能减少内存占用和增强 Java 程序的安全性(AOT 编译后的代码不容易被反编译和修改),特别适合云原生场景。

AOT 编译无法支持 Java 的一些动态特性,如反射、动态代理、动态加载、JNI(Java Native Interface)等。然而,很多框架和库(如 Spring、CGLIB)都用到了这些特性。

2. Java 线程基础

2.1 Java 开线程的方式

实际上,Java 所有开线程的方式归根结底都会汇为一种:

 	new Thread.start();

只不过是给这个 Thread 构造任务的方式有很多而已:

  • 直接继承 Thread 类,重写 run() 方法
  • 实现 Runnable 接口
  • 实现 Callable 接口(FutureTask类),可以拿到线程执行完的返回值
  • 使用线程池的 ExecutorService::submit() 方法或者 Executor::execute() 方法
  • CompletableFuture 执行异步任务
  • 等等……

2.2 wait 和 sleep 的区别

之前一直是 wait 和 notify 一起记的,现在 wait 和 sleep 放一起让我的大脑直接宕机了好几回。真是为了八股而八股啊。

2.2.1 锁对象的 wait 与 notify 机制

wait() 、notify、notifyAll 这三个方法都是被 synchronized 块中关联的锁(monitor)对象调用的方法,定义在 Object 类,因此调用这三个方法是必须在 synchronized 块中的。

  • wait 方法使线程进入等待状态,并对当前线程释放锁。
  • notify 方法唤醒一个等待的线程。
  • notifyAll 方法唤醒所有等待的线程。
	Object lockObject = new Object();

	synchronized (lockObject) {
		while (conditionIsNotMet) {
			lockObject.wait();
		}
		// 继续执行
	}

	synchronized (lockObject) {
		// 修改条件
		conditionIsMet = true;
		lockObject.notifyAll();
	}

// 如果 synchronized 修饰在方法上,锁对象为当前对象,因此直接是 this.wait()
    public synchronized void put(int value) {
        while (available) {
            try {
                wait();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        this.value = value;
        available = true;
        notifyAll();
    }

2.2.2 为什么 sleep() 方法定义在 Thread 中?

因为 sleep() 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。

  • sleep() 方法没有释放锁,而 wait() 方法释放了锁 。
  • wait() 通常被用于线程间交互/通信,sleep()通常被用于暂停执行。
  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。sleep()方法执行完成后,线程会自动苏醒,或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。
  • sleep()Thread 类的静态本地方法,wait() 则是 Object 类的本地方法。
  • 引用自 JavaGuide Thread#sleep() 方法和 Object#wait() 方法对比

2.3 Thread 的生命周期和状态

  • NEW:Thread 对象实例化,但是没有调用 start() ;
  • RUNNABLE:Thread 对象调用 start() 后,操作系统层面 RUNNING 与 READY 的集合;
  • BLOCKED:线程没有获取到 monitor 对象锁,被动阻塞;
  • WAITING:线程主动放弃锁并等待,需要被其他线程唤醒;
  • TIMEED_WAITING:附带超时机制的 WAITING;
  • TERMINATED:线程执行完毕。

2.4 ThreadLocal

  • 面试官:你ThreadLocal都不会你Java并发编程还能会啥?
  • 我:?

2.4.1 用法

ThreadLocal 是 Java 中的一个类,用于在每个线程中存储独立的变量副本,确保线程间的数据隔离。每个线程可以独立访问自己的变量副本,而不会影响其他线程的副本。

使用场景

  • 线程安全:避免多线程共享变量时的竞争条件。
  • 上下文传递:在方法调用链中传递上下文信息,如用户会话、事务 ID 等。

基本用法

  1. 创建 ThreadLocal 变量
  2. 设置值
  3. 获取值
  4. 移除值

示例代码

public class ThreadLocalExample {

    private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        Runnable task = () -> {
            // 设置线程局部变量
            threadLocal.set((int) (Math.random() * 100));
            System.out.println(Thread.currentThread().getName() + " set value: " + threadLocal.get());

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 获取线程局部变量
            System.out.println(Thread.currentThread().getName() + " get value: " + threadLocal.get());

            // 移除线程局部变量
            threadLocal.remove();
        };

        // 创建多个线程
        Thread thread1 = new Thread(task, "Thread-1");
        Thread thread2 = new Thread(task, "Thread-2");

        thread1.start();
        thread2.start();
    }
}

输出示例

Thread-1 set value: 42
Thread-2 set value: 87
Thread-1 get value: 42
Thread-2 get value: 87

注意事项

  • 内存泄漏ThreadLocal 可能导致内存泄漏,尤其是在使用线程池时。线程池中的线程会复用,如果不及时调用 remove() 方法,可能会导致 ThreadLocal 的旧值一直存在,从而引发内存泄漏。
  • 初始值:可以通过重写 initialValue() 方法为 ThreadLocal 提供初始值。
private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>() {
    @Override
    protected Integer initialValue() {
        return 0; // 初始值为 0
    }
};

2.4.2 基本工作原理

Thread 类里面有这么一个属性:

class Thread implements Runnable { 
	/* ThreadLocal values pertaining to this thread. This map is maintained
	 * by the ThreadLocal class. */
	ThreadLocal.ThreadLocalMap threadLocals = null;
}

ThreadLocal 的 get、set 方法实际上就是通过 ThreadLocalMap 的 get、set 方法实现的。

public class ThreadLocal<T> {
  	// ...
  	public T get() {
	    // 获取当前线程的 ThreadLocalMap
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
		  	// 根据当前 ThreadLocal 对象获取值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    public void set(T value) {
	    // 获取当前线程的 ThreadLocalMap
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
		  	// 根据当前 ThreadLocal 对象存入值
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }
}

2.4.3 弱引用机制

ThreadLocalMap 的 Entry 对 Key 使用弱引用,是为了:

  1. 防止 ThreadLocal 对象无法被回收:当开发者不再持有 ThreadLocal 的强引用时,GC 可以回收它。
  2. 触发清理过期 EntryKey 被回收后,ThreadLocalMap 会在后续操作中清理 Key 为 null 的 Entry,释放 Value 的内存。
  3. 开发者仍需注意:显式调用 remove() 方法或在设计上避免线程长期持有 ThreadLocal 数据,才能彻底避免内存泄漏。

ThreadLocalMap 的 节点Entry 对于 key 是弱引用的:

    static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

如果 ThreadLocal 实例不再被任何强引用指向,垃圾回收器会在下次 GC 时回收该实例,导致 ThreadLocalMap 中对应的 key 变为 null。这样 value 就一直是强引用,而如果当前线程一直存活,ThreadLocalMap 与 Entry 对象就一直存在,不会被垃圾回收,最终导致内存泄漏。

也就是说,内存泄漏的发生需要同时满足两个条件:

  1. ThreadLocal 实例不再被强引用;
  2. 线程持续存活,导致 ThreadLocalMap 长期存在。

解决方案:在使用完 ThreadLocal 后,调用 remove() 方法。 remove() 方法会从 ThreadLocalMap 中显式地移除对应的 entry,彻底解决内存泄漏的风险。

2.4.4 线程安全机制

上次面试被问到 ThreadLocal 有锁机制,但是我翻了下源码,只有实例化 ThreadLocal 对象时,其 threadLocalHashCode 属性的初始化用到了 nextHashCode() 静态方法,这个静态方法里面用到了原子类是需要原子递增的。

3. MySQL

3.1 InnoDB 索引为什么用B+树

  • 高扇出,低树高:B+ 树的非叶子节点仅存储键(不存储数据),每个节点可容纳更多键值对,树高更低(通常 3~4 层即可支持千万级数据),减少磁盘 I/O 次数。
  • ​范围查询高效:所有数据存储在叶子节点,且叶子节点通过双向链表连接,范围查询(如 BETWEEN、ORDER BY)只需遍历链表,无需回溯非叶子节点。
  • ​查询稳定:所有查询最终落到叶子节点,时间复杂度稳定为 O(log n),避免 B 树因数据分布在非叶子节点导致的查询性能波动。
  • ​支持全表扫描优化:叶子节点链表结构便于全表顺序扫描,适合大数据量的聚合操作。
  • ​减少锁竞争:行锁和 MVCC 的实现依赖 B+ 树索引结构,支持高并发事务。

3.2 什么情况下不建议使用索引?

  1. 数据唯一性差的字段(如性别只有两种可能数据)不要使用索引
  2. 频繁更新的字段不使用索引,如登录次数
  3. 对索引列使用不等于
  4. 其他索引失效的场景

3.3 InnoDB 页

注意其与操作系统内存管理那个页的区分:

4. Redis 与 缓存

4.1 缓存三问

说实在的,道理我都懂,但是我就是记不住概念,尤其是记混缓存击穿和缓存穿透!

4.2 如何保证缓存与DB的数据一致性?

1. ​Cache-Aside(旁路缓存)​

  • 读流程:先读缓存,命中则返回;未命中则读数据库,写入缓存。
  • 写流程:直接更新数据库,然后删除缓存(非更新缓存)。
  • 关键点:​删除而非更新缓存:避免并发写导致缓存与数据库不一致。​先更新数据库再删缓存:降低脏数据风险(但非完全消除)。

2. ​Write-Through(穿透写)​

  • 写流程:先更新缓存,缓存组件同步更新数据库。
  • 优点:强一致性,缓存即权威数据源。
  • 缺点:性能较差,适合写少读多的场景。

3. ​Write-Behind(异步回写)​

  • 写流程:先更新缓存,异步批量更新数据库。
  • 优点:高吞吐,适合写多场景(如点赞计数)。
  • 缺点:存在数据丢失风险(如缓存宕机)。

4. ​延迟双删(Double Delete)​

  • 场景:应对“先删缓存后更新数据库”时,其他线程读到旧数据并回填缓存。
  • 步骤:删除缓存更新数据库等待短暂时间(如 500ms)再次删除缓存
  • 原理:在数据库主从同步完成后二次清理缓存。

5. Java 集合与数据结构

***,没复习数据结构,真有点难顶吧我说

5.1 数组和链表的区别?

  • 数组:内存分配连续、访问效率适合随机访问、数据量固定、插入删除效率低、有内存浪费或扩容性能损耗;
  • 链表:内存分配不连续、每个节点需要存储指针、高效插入删除、适合频繁增删、数据量动态变化

使用场景:

  • 数组:
  • 图像像素存储:二维数组直接映射像素矩阵,支持快速访问和矩阵运算。
  • 链表:
  • HashMap 链地址法
  • 实现LRU缓存淘汰算法,需频繁调整节点顺序,链表操作效率高于数组。

5.2 红黑树简介

红黑树有四大性质:

  • 左根右:红黑树的前提是二叉搜索树(左 < 根 < 右)
  • 根叶黑:根和叶子节点(NULL)都是黑色
  • 不红红:不存在连续的两个红色节点
  • 黑路同:任一节点到叶子节点所有路径黑节点数量相同

相比 AVL 树,红黑树有如下特征:

  • AVL 树:
  • 任一节点左右子树的高度相差绝对值不超过 1
  • 查询效率高,但是修改性能大
  • 红黑树:
  • 任意节点左右子树高度相差不超过两倍(由“不红红”和“黑路同”两个性质决定)
  • 查询效率略低,但是修改和删除更高效

对于红黑树的插入,也有三条口诀(插入节点默认是红色节点):

  • 插入节点是根节点:直接变黑
  • 插入节点的叔叔是红色:叔父爷变色,爷爷变成插入节点
  • 插入节点的叔叔是黑色(包括NULL):和 AVL 树一样(LL,RR,LR,RL)旋转,然后变色

5.3 数组 & List & Set

使用场景总结

  1. 数组:适用于数据量固定、需高性能访问的场景(如数学计算、底层数据存储)。
  2. List:ArrayList:频繁随机访问,数据量变化较小。LinkedList:频繁增删元素,尤其是中间位置的操作。
  3. Set:HashSet:快速去重,无需顺序。LinkedHashSet:去重且保留插入顺序。TreeSet:去重且需要排序(如按字母、数值排序)。

动态变化、丰富API、泛型支持、框架支持、面向接口等待

优先使用 List:适用于 95% 的日常业务场景,因其动态性、安全性和生态兼容性。

全部评论
我还没开始投呢佬就开始复盘了
点赞 回复 分享
发布于 03-04 10:09 江苏
参考了一部分 JavaGuide 与 B站 蓝不过海呀 的内容~
点赞 回复 分享
发布于 03-02 16:23 山东

相关推荐

04-26 15:09
已编辑
美团_测试开发(准入职员工)
面试官是女生,没开摄像头,进来先介绍技术栈,是否接受php整体30min1.&nbsp;Linux命令,文本处理;只问了我是否熟悉,我说熟悉,没深挖了2.&nbsp;查看正在运行Java的进程;netstat、lsof,面试官回复ps命令呢,我说也用过,和lsof差不多,都可以查看3.&nbsp;有没有用过MySQL集群,如果部署在单机,有没有保证可用性;因为是单体,评估了连接数,并且用了redis减轻压力进行兜底4.&nbsp;提了一嘴RocketMQ,问我是用过的对吧;我说是的,没深挖5.&nbsp;MySQL索引结构;为什么要用B+树;聚簇索引和非聚簇索引6.&nbsp;唯一索引是聚簇还是非聚簇?瞎猜了说是非聚簇,面试官让展开说说,回答是唯一索引只需要判断有没有重复,没必要用到聚簇索引,非聚簇够了。7.&nbsp;WAL技术?没听过,面试官说是数据库当中的预写日志,undo,redo,然后我回答了binlog,redolog,undolog8.&nbsp;CPU突然变高了怎么排查?top命令先定位进程,如果是程序,查看日志,看看是不是死循环了,定位后去修改;感觉没答好。9.&nbsp;springboot注解,MVC机制原理,回答了是通过拦截器拦截所有请求,根据URL去映射Controller10.&nbsp;计网&nbsp;浏览器输入URL整体流程11.&nbsp;大整数相加,不用加法;不会12.&nbsp;合并有序链表;ok13.&nbsp;场景题,抖音的点赞,怎么设计,用到了哪些组件,表结构14.&nbsp;两个文件,一个50w的URL,一个500,找到相同的URL15.&nbsp;反问,能否提前实习
查看15道真题和解析
点赞 评论 收藏
分享
评论
9
49
分享

创作者周榜

更多
牛客网
牛客企业服务