战斗爽!!!对线面试官(面试复盘)

227 阅读10分钟

前言

离职回学校后我又开始了我的面试,接下来我会对每次面试的复盘总结一下分享给大家,希望对大家有帮助!

这场面试总体来说比较简单,没有手撕环节,也都是一些常规的八股。

R-C.gif

具体流程

先让我自我介绍一下,然后问了一下我的具体情况,就开始拷打了

先上八股盛宴:

1.介绍一下ArrayList的原理

先来一道开胃菜

首先ArrayList底层是由动态数组实现的,当用无参构造器创建ArrayList时其初始化容量是0,当第一次加数据时才会初始化容量为10;

ArrayList进行扩容的大小是原来的1.5倍,每次扩容都需要拷贝数组(Arrays.copyOf(),这个过程对于基本数据类型是值拷贝,对于对象引用是引用拷贝)

ArrayList在add元素时的细节:

  • 会先判断已使用的长度+1之后是否足够存下下一个数据
  • 如果当前数组已使用长度+1后大于当前数组长度,则会调用grow方法进行扩容(原来的1.5倍)
  • 确保新增的数据有地方存储之后,就将新元素添加到接下来的位置上并返回true

性能注意:由于ArrayList的底层是数组,所以其查询效率高(O(1)),因此其适合查询多的场景;但是,由于ArrayList的扩容要拷贝数组,所以为了提升性能,如果我们已经知道要存储的元素的数量,可以先指定初始化容量来避免多次扩容。

2.ArrayList扩容后原有元素的顺序会发生改变吗

这点也显而易见了,新数组是拷贝旧数组的,顺序肯定就不会改变了。

3.既然你说ArrayList是线程不安全的,那什么是线程安全的?CopyOnWriteArrayList如何做到线程安全的

由于前面扯到了说ArrayList在添加元素的时候是线程不安全的,为自己买下来坑,面试官就追问:那什么是线程安全的?你介绍一下 (所以我们要谨言慎行,不会的不要为自己埋坑,哈哈哈)

还好我是对这个有所了解的:

CopyOnWriteArrayList其实就是实现了一种读写分离的设计机制

  • 首先CopyOnWriteArrayList底层也是通过数组来实现的
  • 写操作时,首先会对整个add或remove代码块用Synchronized关键字包裹来对其加锁,保证线程安全
  • 其次是写元素时的细节:会先复制一个新的数组(也是通过Arrays.copyOf()拷贝数组),写操作会在这个新的数组上进行,写结束之后把原数组指向新数组
  • 还有就是CopyOnWriteArrayList的读操作是在原数组上进行的,并且允许其在写操作时来读数组,这样没有阻塞读操作,大大提升了读的性能,因此就适合读多写少的场景
  • 不足点就是CopyOnWriteArrayList比较占内存,同时读到的数据不一定是最新的数据,没有强一致性

4.HashMap jdk1.7和后续版本的区别是什么

这种就是老生常谈的八股了,我就不在赘述了

总的来说就是在数据结构上引入了红黑树,插入元素时尾插法代替了头插法解决了并发安全问题,哈希算法也改进了,还有一些其他更具体的方面。

5.ConcurrenHashMap是怎么保证线程安全的

这个也是经常会问到的八股

同样也是分为jdk1.7和jdk1.8之后

jdk1.7:

  • 底层采用的分段数组加链表实现
  • 写操作时采用的是ReentrantLock来锁住这个段,锁粒度较大,性能低
  • 每个Segment段相当于一个小型的HashMap,其扩容机制和HashMap类似

jdk1.8:

  • 底层的数据结构与1.8之后的HashMap一样,都是数组加链表加红黑树
  • 写操作时采用的是Synchronized锁住链表或红黑树的首节点,锁粒度相对更细,性能更好
  • 还有就是1.8版本开始是支持多线程扩容的,当某个线程put元素时,发现ConcurrenHashMap正在扩容那么该线程一起进行扩容
  • 其扩容细节也是先生成一个新的数组,然后在转移元素时,先将原数组分组,再将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组

6.HashMap或者ArrayList for循环调用remove删除时会有问题吗

面试官问到这个的时候,我心想:之前好像在哪里见到过,我只记得要用迭代器去remove,至于为什么不太记得了。(基础不牢地动山摇)

private void fastRemove(int index) {
    modCount++; // 增加修改次数,用于快速失败
    int numMoved = size - index - 1; // 计算需要移动的元素数量
    if (numMoved > 0) {
        System.arraycopy(elementData, index+1, elementData, index, numMoved); // 将index后面的元素向前移动
    }
    elementData[--size] = null; // 将最后一个元素设置为null,帮助GC回收
}

这个fastRemove方法是remove方法内部调用的移除元素的关键逻辑,很明显,每次remove元素,数组的结构就会发生变化,如果在for循环中遍历去调用remove方法,肯定会导致有漏删的元素。因此,我们要采用其他的方法,采用倒叙遍历数组的方法,或采用ArrayList提供了解决方法,其removeIf方法和其迭代器Iterator的remove方法来解决了这个问题,他们对这类问题已经做了处理。

removeIf方法通过记录需要移除的元素索引,然后在一次遍历中统一进行移除,避免了在迭代过程中直接修改集合结构的问题。

而迭代器的remove方法它会处理ArrayList结构的变化,并更新迭代器的状态,确保所有的目标元素都被移除。

7.LinkedList的使用场景是什么

这也是老生常谈的八股了

LinkedList(底层是双向链表来实现的)适用于需要快速插入和删除元素、实现队列、循环列表、栈以及迭代器遍历等场景。

20221011001613_e5a34.gif

8.mysql的四种事务隔离级别是什么

这也是老演员了:读未提交、读已提交、读可重复读、可串行化

9.mysql的三大日志分别是什么介绍一下

redolog:

  • Redo Log是InnoDB存储引擎特有的日志,主要用来保证事务的持久性。当MySQL实例发生故障或宕机后重启时,InnoDB存储引擎会使用Redo Log来恢复数据,确保事务的变更可以被恢复,从而保证数据的完整性和持久性。
  • Redo Log是物理日志,记录的是数据页的物理修改操作。它通过Write-Ahead Logging(WAL)机制,先写日志后写磁盘,确保数据的持久性。
  • Redo Log的刷盘时机可以通过innodb_flush_log_at_trx_commit参数控制,它支持三种策略:每次事务提交时刷盘、每秒刷盘一次、不刷盘。

undolog:

  • Undo Log用于处理事务的原子性和一致性。在事务开始之前,MySQL会记录下数据的初始状态到Undo Log中。如果事务执行过程中需要回滚或数据库崩溃,Undo Log会被用来将数据恢复到事务开始前的状态。
  • Undo Log是逻辑日志,记录的是SQL执行的逆操作。例如,如果一个事务中的INSERT操作失败,Undo Log会记录一个DELETE操作来撤销这个INSERT。

binglog:

  • Bin Log是MySQL Server层的日志,记录了所有修改数据的操作,如INSERT、UPDATE、DELETE等,但不包括SELECT和SHOW等操作。Bin Log主要用于数据恢复和主从复制。
  • Bin Log是逻辑日志,记录的是原始的SQL语句或行变化,属于Server层,与存储引擎无关。
  • Bin Log的写入机制涉及到事务的提交过程,事务提交时会将Bin Log写入到日志文件中。Bin Log的刷盘时机可以通过sync_binlog参数控制。

10.索引下推了解吗

面试官问到这个时,我内心一笑,还好我之前去了解过这个

MySQL的索引下推是MySQL 5.6引入的一项查询优化技术,其主要目的是减少数据访问量和提升查询效率

Snipaste_2024-12-08_21-40-25.png

对于 SELECT * FROM users WHERE name LIKE '张%' AND age = 10;我们对name和age字段建立了一个联合索引,但是这条sql语句由于最左匹配原则,age字段的过滤并不会走索引,会先走索引过滤出所有姓张的人,然后过滤后每条记录都会回表再去过滤出age=10的记录,显然,每一次的回表查询都是很浪费性能的;

因此,在mysql5.6之后,就引入了索引下推的这个机制,这个是默认打开的,有了这个机制后向上面的场景就不会多次的进行回表查询了,会直接在联合索引中进行对age字段的过滤,这样就大大减少了回表的次数,从而提升整体的性能。

11.线程池的几个核心参数是什么

这个也问过几百遍了我就不赘述了

12.核心线程数是什么

这个也是常见问题,就是线程池中始终保持的最小线程数量,它们即使空闲也不会被销毁,以便于快速响应新的任务请求。

13.Snychronized是一个什么类型的锁,介绍一下原理

这也是道经典的八股

Snipaste_2024-12-08_23-10-37.png

我来总结总结:

首先Snychronized是JVM提供的锁,就是在对象的Markword中记录一个锁的状态。从jdk1.6开始,就有了锁升级的机制,首先刚开始当一个线程来获取锁时,JVM会检查该锁是否处于偏向锁状态,如果是无锁状态,就将其设置成偏向锁,并在Markword中记录线程ID,如果是匿名偏向状态就直接设置线程ID;

接着如果另一个线程也来获取这把锁,JVM会先判断线程ID是否相同,不用的话就会尝试撤销这把锁(线程在一定时间内没有活动就可以被撤销),如果偏向锁不能被撤销,JVM就会把其升级成轻量级锁,未获得的线程会CAS一直自旋来获取锁。

之后如果锁竞争变大(即该锁长时间处于竞争状态),JVM会再次将锁升级成重量级锁,这时,未获取到锁的线程不会自旋,而是等待操作系统的调度,操作系统会更具一定的策略来决定谁获取锁。(重量级锁涉及到操作系统,很影响性能,所有jdk1.6之后就有了这个锁升级的过程)

补充:默认JVM在启动时创建对象不会为其加偏向锁(普通对象),而是过了4秒后创建对象才会加偏向锁(匿名偏向,即没有指定线程),这是为了优化JVM的启动速度。

还有就是在锁竞争程度跨度较大时锁的升级是可以越级的。

14.介绍一下TreadLocal、InheritableThreadLocal、TransmittableThreadLocal

面试官问到这个的原因是因为我在掘金中发过一篇这样的文章,因此面试官让我介绍一下(面试官其实会关注你发的文章,可以利用这点来引导面试官的)

感兴趣的朋友可以去看看我的文章。

然后就开始拷打实习:

问了一下项目的背景,实习中做了什么,学习到了什么,遇到了什么棘手的问题怎么处理的,git的一些操作等等这些问题,具体细节就不多透露了。

最后的话就到了反问环节,问了一些公司的项目问题就结束了。

以上文章可能有点小瑕疵,还请大伙见谅。

0d5a868d8f6b2079f590cc7252cd41a93628f7c88613-EiEMRh_fw658.webp