Moka一面
文章目录
前言
记录一下面试流程及问题
自我介绍
巴拉巴拉…
题目一:ArrayList和LinkedList的区别
首先从二者的底层实现上来分析:ArrayList
的底层是动态数组,是一个连续的内存空间存放的数据是连续的;LinkedList
的底层是双向链表,是由一个接着一个的Node节点组成的,Node节点又包括数据、前驱结点、后继结点。
其次从二者的效率上进行分析:ArrayList
更加适合用于多查询,少增删的场景。对于ArrayList
来说他底层实现为动态数组,因此他支持随机查询,根据下标查询数据的时间复杂度为O(1),对于未排序的数据进行查询的最坏时间复杂度为O(n),对于排序后的数据进行查询,时间复杂度为O(logn);LinkedList
更加适合用于增删较多的场景。LinkedList
不支持随机查询,他查询数据的时间复杂度为O(n),增删数据的时间复杂度为O(n),但是如果已知节点,增删他的前驱或后继节点时间复杂度为O(1)
然后从二者的内存利用率上进行分析:ArrayList
内存利用率更高,他使用的是连续的内存空间;LinkedList
利用率低一些,他存储上存储了前驱和后继结点的内存地址,占用的空间是不连续的内存空间,内存利用率不高。
最后从二者的线程安全上进行分析:二者都是线程不安全的。在并发环境下,都不建议使用。如果需要用线程安全的单列集合可以使用Collections工具类下面的synchronizedList
题目二:线程安全的集合
单列集合:Vector、CopyOnWriteArrayList、CopyOnWriteArraySet
双列集合:Hashtable、ConcurrentHashMap
也可以使用Collections工具类下的synchronizedCollection静态方法,它可以将任何 Collection
包装成线程安全的集合。它通过在每个方法上添加同步来实现线程安全。
题目三:HashMap的扩容机制
HashMap的扩容方法为resize(),再添加元素或初始化时,如果当前HashMap中的元素数量已经超过或达到了扩容阈值,即HashMap的长度*加载因子(默认为0.75),就会触发扩容操作。
扩容过程为:HashMap会创建一个新的数组,新数组长度为原数组的两倍,然后依次遍历HashMap的每个元素并将他存入新的数组中,在此过程中会重新进行hash值的计算。
当链表长度达到或者超过8,就会将链表装换为红黑树
追问:为什么要转为红黑树
因为链表太长,查询数据较为缓慢,对发生hash碰撞的元素(链表长度为n)的查询时间达O(n)。红黑树又称为自平衡的二叉搜索树,他的查询时间为O(logn)
题目四:创建线程的方式,实现runnable接口和callable接口的区别
创建线程主要有四种方式:
- 继承Thread类,并重写run方法。
- 实现runnable接口,并实现接口中的run方法
- 实现callable接口,并实现接口中的call方法
- 通过线程池进行创建管理线程(这是我们在项目开发中最合理的方式)
runnable和callable接口有什么区别?
Runnable
和Callable
接口都是Java中用于定义线程任务的接口,但它们之间存在几个关键区别:
-
返回值和异常处理:
Runnable
接口的run()
方法没有返回值,也不支持泛型,且只能抛出运行时异常(RuntimeException)。Callable
接口的call()
方法可以返回一个值,支持泛型,并且可以抛出检查异常(checked exceptions)。
-
与Future的关联:
Runnable
实现的任务通常直接传递给Thread
或Executor
执行,没有直接的方式来获取任务执行的结果。Callable
实现的任务可以被封装在FutureTask
中,这是一个实现了Future
接口的类。通过FutureTask
,可以在另一个线程中获取Callable
任务的执行结果,或者处理任务执行过程中抛出的异常。
-
使用场景:
Runnable
适合于那些不需要返回结果的任务,例如后台处理、事件监听等。Callable
适合于需要返回结果或可能抛出检查异常的任务,例如数据库操作、文件读写等。
-
线程池中的使用:
- 在使用线程池(如
ExecutorService
)时,Runnable
任务通常通过execute()
方法提交,而Callable
任务通过submit()
方法提交。submit()
方法返回一个Future
对象,可以用来获取任务执行的结果。
- 在使用线程池(如
总结来说,Runnable
适用于简单的任务执行,而Callable
提供了更强大的功能,包括返回值和异常处理,使得任务执行的结果可以被后续处理。在设计多线程应用时,应根据任务的具体需求选择合适的接口。
题目五:jvm的内存模型
JVM的内存模型主要包括5个部分。他们分别是:堆、程序计数器、虚拟机栈、本地方法栈、方法区
堆
堆是用于存放对象实例和数组的地方,是垃圾回收的重要场所;堆是线程公用的,是jvm的核心管理区域。
堆中主要分为新生代、老年代。新生代又包括一个Eden区,两个Survivor区。Eden区是对象、数组出生的地方,经过垃圾回收后,存活的对象会放置到Survivor区中。
在java7中,还有一个永久代的概念(在方法区内),经过多次垃圾回收都没有回收掉的对象将被放置到永久代中。所以老年代主要存放的是静态变量、大对象、长期存活的对象等。
在java8时,元空间替代了永久代,不在用jvm内存存放上述替代的数据,改用系统内存存放
程序计数器
程序计数器是用于存储当前线程所执行的字节码指令地址,是线程私有的,用于控制指令的执行。
虚拟机栈
虚拟机栈用于存放java方法的局部变量表、操作数栈、动态链接、方法出口等,是线程私有的,用于支持方法的执行;虚拟机栈中存放的是一个又一个的栈帧。一个方法的执行到结束对应的栈帧入栈到出栈的过程。
本地方法栈
本地方法栈和虚拟机栈有一定的相似。本地方法栈主要是对于java中运用到的本地方法(非java方法,如C或者C++编写的方法),他也是线程私有的
方法区
用于存储类信息、常量、静态变量、即时编译后的代码等数据,是一个线程公用的区域
题目六:堆中常用的垃圾回收算法
垃圾回收算法主要有三种:标记清除算法、复制算法、标记整理算法
标记清除算法
标记清除算法是比较用以理解和实现的算法,他的实现思路为,通过可达性标记法标记需要回收的目标对象,然后再进行清理。
优点:思路简单、实现容易
缺点:会产生内存碎片
复制算法
复制算法的实现思路为将堆划分为两个区域,先在其中一个区域进行对象的创建使用,当其内存不够用时,进行垃圾回收,将需要用到的对象复制到另一半区域中,然后完全清理此区域的对象
优点:简单高效、适用于新生代、减少碎片化内存
缺点:内存浪费、对大对象不友好
标记整理算法
标记整理算法的实现思路为利用可达性算法进行标记,将对象整理至一整块(便于减少内存碎片的出现),将垃圾回收。
优点:减少内存碎片的出现、适用于老年代、内存利用率高
缺点:效率低、复杂度高
题目七:项目中常用的垃圾回收器
-
Serial GC:
- 这是JVM的默认垃圾回收器,适用于单核处理器环境。
- 它在进行垃圾回收时会暂停应用线程(Stop-The-World, STW)。
- Serial GC分为两个部分:Serial(处理新生代)和Serial Old(处理老年代)。
-
Parallel GC:
- 也称为吞吐量优先收集器(Throughput Collector)。
- 它使用多个线程并行处理垃圾回收,以提高回收效率。
- Parallel GC同样分为Parallel Scavenge(处理新生代)和Parallel Old(处理老年代)。
- 适用于多核处理器环境,可以提高应用程序的吞吐量。
-
CMS(Concurrent Mark Sweep)GC:
- 这是一种以最小化停顿时间为目标的垃圾回收器。
- CMS主要处理老年代,它试图在标记和清除阶段与应用线程并发执行。
- CMS在并发标记阶段可能会产生所谓的“浮动垃圾”,这可能导致后续的Full GC。
-
G1(Garbage-First)GC:
- G1 GC是JDK 7中引入的,但在JDK 8中得到了进一步的改进。
- 它旨在处理大型堆内存,并且可以预测停顿时间。
- G1 GC将堆分割成多个区域(Region),并根据垃圾回收的优先级来选择回收哪些区域。
- 它适用于需要低延迟和高吞吐量的应用程序。
-
ZGC(Z Garbage Collector):
- ZGC是JDK 11中引入的实验性垃圾回收器。
- 它旨在实现低延迟和高吞吐量,同时处理大堆内存。
- ZGC使用着色指针和并发标记-清除算法,以减少停顿时间。
在项目中需要考虑程序的因素,如停顿时间的容忍度、堆内存的大小、cpu的核心数等来确定垃圾回收器的选择
题目八:@autowired和@resource的区别
@autowired是Spring提供的一个依赖注入的一个注解;而@resource是jdk提供的一个依赖注入的注解,因此@resource的使用范围更加广泛
@autowired默认情况下会根据类型的方式进行自动装配;而@Resource默认装配是根据名称进行装配
题目九:对IOC的理解
Spring的核心机制在于IOC和AOP,但是在Spring出现之前IOC的思想就已经存在了,Spring只是将其具象化了。Spring的IOC思想就是将Bean对象的管理交给SpringIOC容器来处理,这些Bean主要为被@Bean、@Controller、@Service修饰的类。
题目十:MySQL中的MVCC
mvcc是mysql中一种用于处理并发事务的机制。他能保证多个事务同时访问数据库而不发生冲突。mvcc在InnoDB存储引擎被实现,用于提高数据库的并发性能,同时保证事务的隔离性。
题目十一:MySQL的事务隔离级别
mysql事务的隔离级别分为四级
读未提交(RU)
读未提交是事务最低的隔离级别,他表示事务可以读取到另一个事务未提交的数据。他会出现脏读、不可重复读、幻读的问题
读已提交(RC)
他保证了事务不能读取到其他事务的数据,解决了脏读的错误。但是还是存在不可重复读和幻读的问题。
可重复的(RR)
他保证事务读取的数据不会存在多次读取同一条数据,返回结果不一致。解决了不可重复读的问题,还是存在幻读的问题。
串行化(serialization)
他是最高的事务隔离级别,可以解决幻读的问题(幻读:在增删场景下,多次读取数据得到的记录条数不同)。
mysql的InnoDB存储引擎的默认事务隔离级别是可重复读
题目十二:mysql的事务隔离级别是怎么保证的
MySQL的事务隔离级别是通过一系列的数据库操作和锁定机制来保证的。这些隔离级别定义了事务在执行过程中如何与其他并发事务相互影响,以及系统如何确保事务的原子性、一致性、隔离性和持久性(ACID属性)。以下是MySQL支持的四种隔离级别及其保证方式:
-
读未提交(Read Uncommitted):
- 在这个隔离级别下,事务可以读取到其他未提交事务的更改,因此可能会遇到脏读。
- 这个级别不保证任何类型的隔离,因此也不存在间隙锁或一致性视图。
-
读已提交(Read Committed):
- 在这个隔离级别下,事务只能读取到其他已提交事务的更改,从而避免了脏读。
- 这个级别通过使用一致性非锁定读(consistent non-locking read)来实现。InnoDB存储引擎在这个级别下不使用间隙锁,但会使用一致性视图(ReadView)来保证一致性。
-
可重复读(Repeatable Read):
- 这是MySQL的默认隔离级别。在这个级别下,事务在执行期间可以多次读取相同的数据,而不会因为其他事务的更新而改变结果,从而避免了不可重复读。
- InnoDB通过MVCC和一致性视图(ReadView)来实现这个级别。事务在开始时创建一个ReadView,之后的查询都基于这个视图,即使其他事务提交了更新,当前事务也不会受到影响。但是,这个级别下仍然可能出现幻读。
-
串行化(Serializable):
- 在这个最高隔离级别下,事务完全串行化,事务之间不会相互影响,从而避免了脏读、不可重复读和幻读。
- InnoDB在这个级别下使用锁(包括行锁和表锁)来确保事务的串行执行。这会显著降低并发性能,但在某些需要严格一致性的场景下是必要的。
在可重复读隔离级别下,虽然InnoDB通过MVCC和ReadView来避免不可重复读,但仍然可能发生幻读。为了解决幻读问题,InnoDB使用了间隙锁来锁定一个范围,防止其他事务在这个范围内插入新行。这样,即使其他事务在事务开始后插入了新行,当前事务在后续的查询中也不会看到这些新行。
需要注意的是,不同的隔离级别对数据库性能有不同的影响。较低的隔离级别(如读未提交)可能会提高并发性能,但牺牲了数据的一致性。较高的隔离级别(如串行化)提供了更强的数据一致性保证,但可能会降低系统的并发性能。因此,选择合适的隔离级别需要在性能和一致性之间做出权衡。
题目十三:联表查询
面试官给了一个简单的连表查询的题目
题目十四:MySQL索引类型
mysql的索引可以分为聚簇索引和非聚簇索引
聚簇索引
聚簇索引是的叶子结点包含完整的数据行,通常数据表中的主键可作为聚簇索引,(若未设定主键,则对唯一字段设立聚簇索引;若没有唯一字段,系统会对mvcc提供的隐藏字段设立聚簇索引),聚簇索引只能有一个
非聚簇索引(二级索引)
非聚簇索引有多个,非聚簇索引的叶子结点包括索引键值和主键,查询时会根据主键去聚簇索引查询完整的数据行(这个现象被称为回表)。
非聚簇索引包括以下索引:
复合索引是指多个字段联合构成的一个索引。
唯一索引指对具有唯一性字段建立的索引,允许一些Null的存在
普通索引指用于加速查询的索引列
题目十五:实习经历及难点
balbalabala
题目十六:算法题
public class DepartmentTest {
public static void main(String[] args) {
List<Department> allDepartment = new ArrayList<>();
Department dep1 = new Department(1, 0, "北京总部");
Department dep3 = new Department(3, 1, "研发中心");
Department dep4 = new Department(4, 3, "后端研发组");
Department dep6 = new Department(6, 4, "后端实习生组");
Department dep7 = new Department(7, 3, "前端研发组");
Department dep8 = new Department(8, 1, "产品部");
allDepartment.add(dep6);
allDepartment.add(dep7);
allDepartment.add(dep8);
allDepartment.add(dep1);
allDepartment.add(dep3);
allDepartment.add(dep4);
List<Department> subDepartments = DepartmentTest.getSub(3, allDepartment);
for (Department subDepartment : subDepartments) {
System.out.println(subDepartment);
}
}
/**
* 根据id,获取所有子部门列表(包括隔代子部门,一直到叶子节点)
* 要求:不能新增参数,不能增加static变量
* @param id
* @return
*/
public static List<Department> getSub(int id, List<Department> allDepartment) {
return null;
}
}
class Department {
/** id */
private int id;
/** parent id */
private int pid;
/** 名称 */
private String name;
public Department(int id, int pid, String name) {
this.id = id;
this.pid = pid;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getPid() {
return pid;
}
public void setPid(int pid) {
this.pid = pid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Department{" +
"id=" + id +
", pid=" + pid +
", name='" + name + '\'' +
'}';
}
}
一道简单的递归题
题解:
/**
* 根据id,获取所有子部门列表(包括隔代子部门,一直到叶子节点)
* 要求:不能新增参数,不能增加static变量
* @param id
* @return
*/
public static List<Department> getSub(int id, List<Department> allDepartment) {
List<Department> result = new ArrayList<>();
// 找到所有父ID与给定ID相匹配的部门
for (Department dep : allDepartment) {
if (dep.getPid() == id) {
result.add(dep);
// 递归获取子部门的子部门
List<Department> subSubDepartments = getSub(dep.getId(), allDepartment);
result.addAll(subSubDepartments);
}
}
return result;
}