准备很久,还是被蚂蚁虐了!

2023-12-13 15:27:49 浏览数 (1)

哈喽~,大家好,我是千羽。

下面分享我认识的一位24届大佬华中科技大学985硕,蚂蚁一面。

但是我个人认为这个面试就是一场KPI面,面试官连我是实习的都不知道,以为我是应届春招生...不过面试官人很好,我在说项目的时候一直'嗯、嗯'来回应,感觉面试体验比较不错~,没想到后面一面也挂了


  • 1、自我介绍
  • 2、项目介绍(抖音项目),自底向上所有的设计思路都讲述了一遍
  • 3、Java类加载器(class Loader)
  • 4、Java多线程之间的同步方式(synchronized, ReentrantLock以及底层设计原理)
  • 5、Java虚拟机的garbage collection,分代GC不同代是如何划分的(我说了Golang里面的GC原理,然后类比)
  • 6、Java线程池参数、线程池调度方式(这个我说了Golang的GPM模型)
  • 7、Http1.1的长连接如何实现的(TCP连接默认不关闭,可以被多个连接复用)
  • 8、那么如何理解Http是一个无状态的连接协议?(无状态是针对客户端和服务端的,而不是针对连接的,我的理解是这样,然后我说如果要添加状态可以加入Cookie)
  • 9、Cookie和Session的区别(针对客户端的和针对服务端的)
  • 10、TCP的三次握手(分别做了什么),四次挥手呢
  • 11、MySQL 4种隔离级别,分别解决了什么问题
  • 12、Mysql中的事务是如何实现ACID的特性的(A:undo log,C:通过事务本身以及业务场景,I:MVCC,D:redo log)

1、自我介绍

2、项目介绍(抖音项目),自底向上所有的设计思路都讲述了一遍

3、Java类加载器(class Loader)

Java类加载器(ClassLoader)是Java虚拟机的一部分,它的主要任务是动态地加载Java类到Java虚拟机中。类加载器在Java虚拟机启动时,通过读取系统类路径(classpath)来加载Java的核心类,如Object类、String类等。在Java虚拟机运行期间,类加载器负责根据程序员指定的类名,将定义该类的字节码文件加载到Java虚拟机中,生成对应的Java类。

Java类加载器主要有三种:

  1. 引导类加载器(Bootstrap Class Loader):负责加载Java的核心类,如Object类、String类等。这些类位于Java的核心库中,如rt.jar、resources.jar等。引导类加载器是Java虚拟机实现的一部分,不是通过继承ClassLoader来实现的。
  2. 扩展类加载器(Extension Class Loader):负责加载Java的扩展类。它的父加载器是引导类加载器。扩展类加载器是通过继承ClassLoader来实现的。
  3. 系统类加载器(System Class Loader):也称应用类加载器(Application Class Loader),负责加载应用程序的类。系统类加载器是默认的类加载器,可以由用户自定义类加载器,然后通过ClassLoader的defineClass方法来加载类。

Java类加载器有以下几个特点:

  1. 分层式类加载器:Java类加载器采用了分层的类加载机制,每一层都只加载自己需要加载的类,避免了不必要的加载和冲突。
  2. 委托式加载:Java类加载器采用了委托式加载机制,即先由父类加载器加载,只有父类加载器无法加载时才由自己的类加载器尝试加载。这种机制有利于保证类的一致性和兼容性。
  3. 动态式链接:Java类加载器采用了动态式链接机制,即在运行时根据需要动态地解析和链接类的二进制数据,这样可以避免在编译时生成大量的.class文件。
  4. 双亲委派模型:Java类加载器采用了双亲委派模型,即当一个类加载器收到了类的加载请求时,它不会自己首先去加载,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的引导类加载器中。只有当父类加载器无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。这种模型有效地保证了Java核心API的稳定性和防止了一些安全问题。

4、Java多线程之间的同步方式(synchronized, ReentrantLock以及底层设计原理)

Java中的多线程同步方式主要有两种:synchronized关键字和ReentrantLock类。这两种方式都可以用来保证多线程并发执行时数据的一致性和线程安全。

  1. synchronized关键字:

synchronized是Java提供的一种内置的线程同步机制。它可以用来修饰方法或者以代码块的形式出现。当多个线程访问某个类的synchronized方法或者代码块时,它们会串行执行,即在同一时刻只有一个线程可以执行该方法或者代码块。

在底层设计上,synchronized是基于对象头的锁来实现的。每个对象在JVM中都有一个对象头,其中包含了对象的hashCode、锁信息等内容。当一个线程尝试访问某个对象的synchronized方法或者代码块时,它会先尝试获取该对象的锁,如果锁已经被其他线程占用,则当前线程会被阻塞,直到获取到锁为止。

  1. ReentrantLock类:

ReentrantLock是Java并发包(java.util.concurrent.locks)中提供的一种可重入锁,它实现了Lock接口。相比于synchronizedReentrantLock提供了更多的灵活性和扩展性。

ReentrantLock提供了两个主要的方法:lock()unlock()。当一个线程调用lock()方法时,它会尝试获取锁,如果锁已经被其他线程占用,则当前线程会被阻塞,直到获取到锁为止。当一个线程调用unlock()方法时,它会释放锁,使得其他等待该锁的线程有机会获取到锁。

在底层设计上,ReentrantLock是基于AQS(AbstractQueuedSynchronizer)框架来实现的。AQS是一个用来构建锁或者其他同步组件的基础框架,它使用了一个int类型的状态来表示同步状态,通过CAS操作来实现状态的修改和比较。当一个线程尝试获取锁时,它会先通过CAS操作来尝试修改状态,如果修改成功,则表示获取到了锁;如果修改失败,则表示锁已经被其他线程占用,当前线程会被封装成一个Node节点并加入到等待队列中等待锁的释放。

总的来说,synchronizedReentrantLock都是Java中用来实现多线程同步的方式,它们都可以保证多线程并发执行时数据的一致性和线程安全。但是它们在使用方式和底层实现上有所不同,需要根据具体的场景和需求来选择使用哪种方式。

5、Java虚拟机的garbage collection,分代GC不同代是如何划分的(我说了Golang里面的GC原理,然后类比)

Java虚拟机(JVM)中的垃圾收集(GC)机制是自动内存管理的核心部分。与Golang的垃圾回收机制类似,Java的垃圾收集器也采用了分代收集策略。不过,Java的垃圾收集器将堆内存划分为四个世代(Generation),而Golang的垃圾回收则主要关注堆内存和栈内存。

以下是Java虚拟机中垃圾收集的分代:

  1. 新生代(Young Generation):新生代是JVM中存放新创建的对象所在的区域。它由两个更小的部分组成:Eden区和两个Survivor区(Survivor0和Survivor1)。大部分对象都在Eden区中创建,当Eden区满时,会触发Minor GC(G1GC),清理掉不再使用的对象,将仍被使用的对象移动到Survivor区。Survivor区满时,仍被使用的对象会被移动到另一个Survivor区或老年代(Old Generation)。
  2. 老年代(Old Generation):老年代主要存放生命周期较长的对象。当老年代空间快满时,会触发Major GC(Full GC),清理不再使用的对象,对于仍被使用的对象,根据其存活时间、大小等特征,有些会继续在老年代中,有些会被移到另一块更大的空间即PermGen(永久代)或Metaspace(元空间)中。
  3. 永久代(PermGen)或元空间(Metaspace):永久代或元空间用于存储类的元数据信息。在Java 8之前,永久代是JVM的一部分,但在Java 8中,永久代被元空间(Metaspace)替代。元空间位于堆外,因此不会受到JVM的堆内存限制。当元空间满时,会触发Full GC。
  4. 方法区(Method Area):方法区用于存储已被JIT编译的代码。它不属于堆内存的一部分,但会受到JVM的内存限制。方法区的内存溢出也会触发Full GC。

当JVM启动时,所有的对象都会进入新生代。经过几次Minor GC后,如果某些对象被证明是长寿对象,它们将被移到老年代。在老年代中,对象可能会一直存活,直到它们被显式地删除或GC算法判断它们不再有用为止。

6、Java线程池参数、线程池调度方式(这个我说了Golang的GPM模型)

Java线程池的参数和线程池的调度方式是线程池配置和使用的关键部分。下面是一些主要的参数和调度方式:

线程池参数:

  1. 核心线程数(corePoolSize): 线程池的核心线程数,这些线程始终保持在池中,即使它们处于空闲状态。
  2. 最大线程数(maximumPoolSize): 线程池的最大线程数。如果队列满了,而且当前线程数小于最大线程数,则创建新的线程执行任务。
  3. 队列容量(queue.capacity): 队列用于存放等待执行的任务,如果队列满了,则无法接受新任务。
  4. 线程工厂(ThreadFactory): 用于创建新线程的工厂。可以通过实现ThreadFactory接口或使用默认的Executors的一些工厂方法创建。
  5. 拒绝策略(RejectedExecutionHandler): 当任务被拒绝时,即当队列满且当前线程数达到最大时,该策略将被触发。Java提供了一些默认的拒绝策略,如AbortPolicy(抛异常)、CallerRunsPolicy(调用者执行任务)、DiscardOldestPolicy(丢弃队列中的最旧任务)和DiscardPolicy(不执行任何操作)。

线程池调度方式:

Java线程池主要有以下几种调度方式:

  1. 直接提交(CallerRuns): 如果当前的线程数小于corePoolSize,则调用者将运行任务。否则,如果任务被提交到线程池,它将被添加到队列中。如果队列已满,则创建一个新的线程(只要当前线程数小于maximumPoolSize)。如果所有的核心线程都处于活动状态,且队列也已满,则调用RejectedExecutionHandler的reject方法。这是默认的策略。
  2. 无界队列、直接拒绝(Abort): 如果当前的线程数小于corePoolSize,则调用者将运行任务。否则,如果任务被提交到线程池,它将被添加到队列中。如果队列已满,则调用者将抛出一个运行时异常。这是AbortPolicy的默认行为。
  3. 有界队列、无界拒绝(CallerRuns): 如果当前的线程数小于corePoolSize,则调用者将运行任务。否则,如果任务被提交到线程池,它将被添加到队列中。如果队列已满,则创建一个新的线程(只要当前线程数小于maximumPoolSize)。如果所有的核心线程都处于活动状态,且队列也已满,则调用RejectedExecutionHandler的reject方法。这是ThreadPoolExecutor的默认策略。
  4. 有界队列、无界拒绝(DiscardOldest): 如果当前的线程数小于corePoolSize,则调用者将运行任务。否则,如果任务被提交到线程池,它将被添加到队列中。如果队列已满,则丢弃队列中最旧的任务并返回false。如果能够执行新任务则返回true。
  5. 有界队列、无界拒绝(Abort): 如果当前的线程数小于corePoolSize,则调用者将运行任务。否则,如果任务被提交到线程池,它将被添加到队列中。如果队列已满,则调用者将抛出一个运行时异常。这是ThreadPoolExecutor的默认行为。

7、Http1.1的长连接如何实现的(TCP连接默认不关闭,可以被多个连接复用)

HTTP/1.1的长连接实现的核心思想是复用已经存在的TCP连接,以减少建立和断开连接带来的开销。在HTTP/1.1中,TCP连接默认不关闭,可以被多个请求复用。

HTTP/1.1的长连接(也称为持久连接或Keep-Alive连接)是通过以下几个机制实现的:

  1. Connection头字段:在HTTP/1.0中,默认的连接方式是短连接,即每次请求都需要建立一个新的TCP连接,请求处理完毕后立即断开连接。为了在HTTP/1.0中实现长连接,可以在请求的头部字段中加入Connection: keep-alive。在HTTP/1.1中,默认就是长连接,但如果需要关闭长连接,可以使用Connection: close
  2. Keep-Alive头字段:当使用长连接时,服务器可以使用Keep-Alive头字段来指定一个超时时间,在这个时间段内,如果客户端有新的请求,可以复用已经存在的TCP连接。这样可以避免频繁地建立和断开TCP连接带来的开销。Keep-Alive头字段可以包含最大连接数和超时时间等信息。
  3. 管线化(Pipelining):HTTP/1.1还引入了管线化技术,即在同一个TCP连接中,客户端可以连续发送多个HTTP请求,而不需要等待每个请求的响应。这样可以降低网络延迟,提高请求的处理速度。但需要注意的是,不是所有的服务器和代理都支持管线化。

总的来说,HTTP/1.1的长连接是通过TCP连接复用和管线化等技术来实现的,这可以提高网络应用的性能和效率。

8、那么如何理解Http是一个无状态的连接协议?(无状态是针对客户端和服务端的,而不是针对连接的,我的理解是这样,然后我说如果要添加状态可以加入Cookie)

HTTP是一个无状态的连接协议,这意味着HTTP协议对于事务处理没有记忆能力。具体来说,当客户端向服务器发送一个请求时,服务器会对该请求进行处理并返回响应,但服务器不会记录之前处理过的请求或响应。因此,每次连接都是独立的,之前连接的状态对后续连接没有影响。

这种无状态性对于实现一些应用程序造成了一些问题。例如,在Web应用程序中,用户可能会在多个页面之间进行导航,或者在多个会话之间进行交互。在这种情况下,服务器需要跟踪用户的状态,以便在多个请求之间保持一致性。为了解决这个问题,HTTP协议使用了一些技术来维护状态,如Cookie和Session。这些技术允许服务器跟踪用户的状态,并在多个请求之间保持一致性。

然而,HTTP本身仍然是一个无状态的协议。为了支持有状态的应用程序,我们需要通过其他技术来维护状态,如Cookie和Session。这些技术允许我们在客户端和服务器之间存储和检索状态信息,以便在多个请求之间保持一致性。因此,虽然HTTP本身是无状态的,但我们可以通过其他技术来实现有状态的应用程序。

9、Cookie和Session的区别(针对客户端的和针对服务端的)

Cookie和Session都是为了维护用户状态而创建的,它们的主要区别在于存储位置、存储数据类型、隐私策略、有效期和服务器压力等方面。

  1. 存储位置:Cookie存储在客户端,而Session存储在服务器上。
  2. 存储数据类型:Cookie只能保存ASCII字符串,而Session中可以保存任意类型的数据,包括Java Bean等。
  3. 隐私策略:由于Cookie存储在客户端,因此对客户端是可见的,可以被修改、复制或窥探。而Session存储在服务器上,不存在敏感信息泄露的风险。
  4. 有效期:Cookie的过期时间可以被设置很长,而Session依赖于名为JSESSIONI的Cookie,其过期时间默认为-1,只要关闭了浏览器窗口,该Session就会过期。
  5. 服务器压力:每个用户都会产生一个session,如果并发访问的用户过多,就会产生非常多的session,耗费大量的内存。而Cookie则不会有这个问题。
  6. 浏览器支持:所有的现代浏览器都支持Cookie,但一些老版本的浏览器可能不支持Session。

总的来说,Cookie和Session都有各自的优点和缺点。在选择使用时,需要根据具体的应用场景和需求来决定。例如,对于需要存储大量数据且对安全性要求较高的应用,可以选择使用Session;而对于一些轻量级的应用或对用户体验要求较高的应用,可以选择使用Cookie。

10、TCP的三次握手(分别做了什么),四次挥手呢

TCP的三次握手和四次挥手的过程如下:

三次握手:

  1. 第一次握手:客户端发送SYN(同步序列编号)包向服务器发起连接请求,进入SYN_SENT状态,等待服务器响应。
  2. 第二次握手:服务器收到SYN包后,向客户端发送一个SYN-ACK包,表示已监听到连接请求并给予反馈。同时,服务器进入SYN_RECV状态。
  3. 第三次握手:客户端收到服务器的确认信息后,向服务器发送ACK确认包。发送完毕后,客户端和服务器进入ESTABLISHED状态,完成三次握手。三次握手的目的是建立TCP连接,同步连接双方的序列号和确认号,并交换TCP窗口大小信息。

四次挥手:

  1. 客户端向服务器发送一个FIN(结束)包,表示要与服务器断开连接,进入FIN_WAIT_1状态。
  2. 服务器收到FIN包后,向客户端发送确认信息,同时自己进入CLOSE_WAIT状态。
  3. 客户端收到服务器的确认信息后,进入FIN_WAIT_2状态,并等待服务器发送释放连接的报文。
  4. 服务器发送完释放连接的报文后进入TIME_WAIT状态,等待一段时间以确保连接完全释放。之后进入CLOSED状态。客户端收到服务器发送的确认信息后也进入CLOSED状态。完成四次挥手。

11、MySQL 4种隔离级别,分别解决了什么问题

MySQL的4种隔离级别包括读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。这些隔离级别主要解决了以下问题:

  1. 读未提交(Read Uncommitted):这是最低的隔离级别,它允许事务读取尚未提交的其他事务所做的修改。这种级别可能导致脏读(读取到其他事务未提交的数据)、不可重复读(在同一事务中多次读取同一数据返回的结果有所不同)和幻读(当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录)等问题。
  2. 读已提交(Read Committed):这是大多数数据库系统的默认隔离级别。它只能读取已提交的数据,解决了脏读问题。但是,这个级别仍然可能出现不可重复读和幻读的问题。
  3. 可重复读(Repeatable Read):在这个级别,同一事务的多个实例在并发读取数据时,会看到同样的数据行。解决了不可重复读的问题。但是,仍然可能出现幻读的问题。
  4. 串行化(Serializable):这是最高的隔离级别。它通过强制事务串行执行,避免了前面提到的所有并发问题。这种级别可能会严重影响程序的性能,因为当多个事务尝试同时访问同一数据时,它们会被阻塞并等待其他事务完成。

12、Mysql中的事务是如何实现ACID的特性的(A:undo log,C:通过事务本身以及业务场景,I:MVCC,D:redo log)

MySQL使用undo log、MVCC(多版本并发控制)和redo log等技术来实现ACID特性。

  1. Undo log:Undo log是MySQL中用于回滚操作的重要机制。在事务执行过程中,如果发生错误或者需要撤销某个操作,MySQL可以使用undo log来还原数据到事务开始前的状态。Undo log记录了每个修改操作的逆操作,即如何撤销该修改。当事务需要回滚时,MySQL通过读取undo log并执行其中的逆操作来撤销对数据的修改。Undo log保证了事务的原子性和一致性。
  2. MVCC:MVCC是MySQL中实现并发控制的一种机制。它允许多个事务同时对数据进行读取和修改,而不会相互干扰。MVCC通过在每个数据行上维护多个版本,每个版本对应一个事务的查看快照。当一个事务修改数据时,它只修改自己查看的快照中的数据行版本,而不会直接影响其他事务的快照。这样,多个事务可以同时进行,而不会相互阻塞。MVCC保证了隔离性和一致性。
  3. Redo log:Redo log是MySQL中用于保证数据持久性的重要机制。在事务提交时,MySQL会将修改操作先写入redo log,然后在合适的时机将修改应用到实际的数据文件中。Redo log记录了每个修改操作的日志,它保证了即使在系统崩溃或发生故障的情况下,已提交的修改也不会丢失。Redo log与undo log共同协作,实现了数据的持久性和一致性。

综上所述,MySQL通过undo log、MVCC和redo log等技术实现了ACID特性。Undo log用于回滚操作,保证了原子性和一致性;MVCC实现并发控制,保证了隔离性和一致性;redo log用于保证数据持久性,实现了数据的持久性和一致性。这些技术协同工作,确保了在并发环境下数据的一致性、隔离性和持久性。

  • 原文链接:https://github.com/warthecatalyst/What-to-in-Graduate-School/blob/main/秋招的面经/华科计科第二人的秋招报告.md

0 人点赞