前言
离职回学校后我又开始了我的面试,接下来我会对每次面试的复盘总结一下分享给大家,希望对大家有帮助!
这场面试总体来说比较简单,没有手撕环节,也都是一些常规的八股。
具体流程
先让我自我介绍一下,然后问了一下我的具体情况,就开始拷打了
先上八股盛宴:
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(底层是双向链表来实现的)适用于需要快速插入和删除元素、实现队列、循环列表、栈以及迭代器遍历等场景。
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引入的一项查询优化技术,其主要目的是减少数据访问量和提升查询效率
对于 SELECT * FROM users WHERE name LIKE '张%' AND age = 10;我们对name和age字段建立了一个联合索引,但是这条sql语句由于最左匹配原则,age字段的过滤并不会走索引,会先走索引过滤出所有姓张的人,然后过滤后每条记录都会回表再去过滤出age=10的记录,显然,每一次的回表查询都是很浪费性能的;
因此,在mysql5.6之后,就引入了索引下推的这个机制,这个是默认打开的,有了这个机制后向上面的场景就不会多次的进行回表查询了,会直接在联合索引中进行对age字段的过滤,这样就大大减少了回表的次数,从而提升整体的性能。
11.线程池的几个核心参数是什么
这个也问过几百遍了我就不赘述了
12.核心线程数是什么
这个也是常见问题,就是线程池中始终保持的最小线程数量,它们即使空闲也不会被销毁,以便于快速响应新的任务请求。
13.Snychronized是一个什么类型的锁,介绍一下原理
这也是道经典的八股
我来总结总结:
首先Snychronized是JVM提供的锁,就是在对象的Markword中记录一个锁的状态。从jdk1.6开始,就有了锁升级的机制,首先刚开始当一个线程来获取锁时,JVM会检查该锁是否处于偏向锁状态,如果是无锁状态,就将其设置成偏向锁,并在Markword中记录线程ID,如果是匿名偏向状态就直接设置线程ID;
接着如果另一个线程也来获取这把锁,JVM会先判断线程ID是否相同,不用的话就会尝试撤销这把锁(线程在一定时间内没有活动就可以被撤销),如果偏向锁不能被撤销,JVM就会把其升级成轻量级锁,未获得的线程会CAS一直自旋来获取锁。
之后如果锁竞争变大(即该锁长时间处于竞争状态),JVM会再次将锁升级成重量级锁,这时,未获取到锁的线程不会自旋,而是等待操作系统的调度,操作系统会更具一定的策略来决定谁获取锁。(重量级锁涉及到操作系统,很影响性能,所有jdk1.6之后就有了这个锁升级的过程)
补充:默认JVM在启动时创建对象不会为其加偏向锁(普通对象),而是过了4秒后创建对象才会加偏向锁(匿名偏向,即没有指定线程),这是为了优化JVM的启动速度。
还有就是在锁竞争程度跨度较大时锁的升级是可以越级的。
14.介绍一下TreadLocal、InheritableThreadLocal、TransmittableThreadLocal
面试官问到这个的原因是因为我在掘金中发过一篇这样的文章,因此面试官让我介绍一下(面试官其实会关注你发的文章,可以利用这点来引导面试官的)
感兴趣的朋友可以去看看我的文章。
然后就开始拷打实习:
问了一下项目的背景,实习中做了什么,学习到了什么,遇到了什么棘手的问题怎么处理的,git的一些操作等等这些问题,具体细节就不多透露了。
最后的话就到了反问环节,问了一些公司的项目问题就结束了。
以上文章可能有点小瑕疵,还请大伙见谅。