面试官:请简要介绍一下Java核心知识中的面向对象编程概念。
王铁牛:面向对象编程主要有封装、继承、多态。封装就是把数据和操作数据的方法封装在一起,对外提供统一的接口;继承是子类继承父类的属性和方法;多态就是同一个行为具有多个不同表现形式或形态。
面试官:嗯,回答得不错。那说说JUC里的并发工具类CountDownLatch的作用和使用场景。
王铁牛:CountDownLatch可以让一个线程等待其他多个线程完成任务后再执行。比如在一个多线程任务中,主线程需要等待所有子线程都计算完结果后,再进行汇总操作,就可以用CountDownLatch。
面试官:很好。再讲讲JVM的内存区域划分。
王铁牛:JVM内存区域主要有堆、栈、方法区、程序计数器、本地方法栈。堆是存放对象实例的地方;栈是存储局部变量和方法调用的地方;方法区存放类信息、常量、静态变量等;程序计数器记录当前线程执行的字节码指令地址;本地方法栈用于执行本地方法。
第一轮结束。
面试官:多线程中如何解决线程安全问题?
王铁牛:可以用synchronized关键字加锁,也可以用Lock接口及其实现类。
面试官:那线程池有哪些参数,分别有什么作用?
王铁牛:线程池有corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler这些参数。corePoolSize是核心线程数;maximumPoolSize是最大线程数;keepAliveTime是线程池线程数超过corePoolSize时,多余线程在多长时间后会被销毁;unit是keepAliveTime的时间单位;workQueue是任务队列;threadFactory是线程工厂;handler是任务拒绝策略。
面试官:说说HashMap的底层实现原理。
王铁牛:HashMap底层是数组+链表+红黑树结构。当链表长度超过8且数组长度大于64时,链表会转换为红黑树。插入元素时,先计算哈希值,然后根据哈希值找到对应的桶位置,如果桶为空就直接插入,如果桶不为空就遍历链表或红黑树找到相同key的节点进行更新,找不到就插入新节点。
第二轮结束。
面试官:Spring的核心特性有哪些?
王铁牛:Spring的核心特性有依赖注入、面向切面编程、IoC容器。依赖注入可以将对象之间的依赖关系通过配置的方式注入;面向切面编程可以在不修改原有代码的基础上,动态地添加功能;IoC容器负责创建、管理和装配对象。
面试官:Spring Boot的自动配置原理是什么?
王铁牛:Spring Boot通过@EnableAutoConfiguration注解开启自动配置功能。它会根据类路径下的依赖和配置类,自动配置各种组件和属性。比如当引入了Spring Data JPA的依赖,就会自动配置JPA相关的组件。
面试官:MyBatis的缓存机制是怎样的?
王铁牛:MyBatis有一级缓存和二级缓存。一级缓存是SqlSession级别的缓存,同一个SqlSession查询的数据会先从一级缓存中获取,减少数据库查询。二级缓存是namespace级别的缓存,多个SqlSession可以共享二级缓存。
面试官:Dubbo的服务调用原理是什么?
王铁牛:Dubbo服务调用时,消费者通过远程代理对象调用服务,代理对象会通过网络向注册中心获取服务提供者的地址列表,然后根据负载均衡策略选择一个提供者进行远程调用,调用过程中会进行序列化、反序列化等操作。
第三轮结束。
面试结束,面试官表示会让王铁牛回家等通知。
答案:
- Java核心知识中的面向对象编程概念:
- 封装:把数据和操作数据的方法封装在一起,对外提供统一的接口。这样可以隐藏内部实现细节,提高代码的安全性和可维护性。例如,一个类中的私有成员变量,只能通过该类提供的公共方法来访问和修改。
- 继承:子类继承父类的属性和方法。通过继承可以实现代码复用,子类可以继承父类的共性,然后在此基础上添加自己特有的属性和方法。比如一个Animal类有name属性和eat方法,Dog类继承Animal类,就可以直接拥有name属性和eat方法,还能添加自己的特有方法,如bark。
- 多态:同一个行为具有多个不同表现形式或形态。多态分为编译时多态(方法重载)和运行时多态(方法重写)。方法重载是在同一个类中定义多个同名但参数列表不同的方法,调用时根据参数类型和数量来决定调用哪个方法。方法重写是子类重写父类的方法,在运行时根据对象的实际类型来决定调用哪个方法。例如,父类有一个方法show,子类重写了该方法,当用子类对象调用show方法时,实际调用的是子类重写后的方法。
- JUC里的并发工具类CountDownLatch的作用和使用场景:
- 作用:可以让一个线程等待其他多个线程完成任务后再执行。它内部维护一个计数器,通过调用countDown方法来减少计数器的值,当计数器的值为0时,等待的线程会被唤醒继续执行。
- 使用场景:比如在一个多线程任务中,主线程需要等待所有子线程都计算完结果后,再进行汇总操作。可以创建一个CountDownLatch,其计数器初始值为子线程的数量,每个子线程执行完任务后调用countDown方法,主线程调用await方法等待,直到计数器变为0,主线程再继续执行汇总操作。
- JVM的内存区域划分:
- 堆:是存放对象实例的地方。它是JVM中最大的一块内存区域,被所有线程共享。堆内存又可以分为新生代、老年代和永久代(Java 8后为元空间)。新生代主要存放新创建的对象,老年代存放经过多次垃圾回收后仍然存活的对象,永久代(元空间)存放类信息、常量、静态变量等。
- 栈:存储局部变量和方法调用。每个线程都有自己独立的栈空间,栈帧中存放着方法的局部变量表、操作数栈、动态链接、方法出口等信息。当一个方法被调用时,会在栈中创建一个新的栈帧,方法执行完毕后,栈帧会被销毁。
- 方法区:存放类信息、常量、静态变量等。它也是被所有线程共享的内存区域。在Java 8之前,方法区被称为永久代,使用的是JVM的堆内存;Java 8后,方法区被元空间取代,元空间使用的是本地内存。
- 程序计数器:记录当前线程执行的字节码指令地址。它是线程私有的内存区域,也是JVM中最小的一块内存区域。程序计数器的作用是保证线程切换后能恢复到正确的执行位置。
- 本地方法栈:用于执行本地方法。本地方法是用C或C++实现的方法,通过JNI(Java Native Interface)与Java代码进行交互。本地方法栈中存放着本地方法的调用信息。
- 多线程中解决线程安全问题的方法:
- synchronized关键字加锁:可以修饰方法或代码块。当一个线程访问被synchronized修饰的方法或代码块时,会先获取对象的锁,如果锁被其他线程占用,该线程会进入等待状态,直到锁被释放。例如,在一个类中有一个被synchronized修饰的方法,当多个线程同时调用这个方法时,同一时间只有一个线程能执行该方法,其他线程需要等待。
- Lock接口及其实现类:如ReentrantLock。Lock接口提供了比synchronized更灵活的锁控制。可以通过lock方法手动获取锁,unlock方法手动释放锁,还可以使用tryLock方法尝试获取锁,避免死锁。例如,使用ReentrantLock时,可以在获取锁之前先尝试使用tryLock方法,如果在指定时间内获取不到锁,可以执行其他操作,而不是一直等待。
- 线程池的参数及其作用:
- corePoolSize:核心线程数。线程池创建后,默认会先创建corePoolSize个线程来处理任务。当提交的任务数小于corePoolSize时,会直接创建新线程来执行任务。
- maximumPoolSize:最大线程数。当提交的任务数大于corePoolSize时,会将任务放入workQueue中,如果workQueue已满,且当前线程数小于maximumPoolSize,则会创建新线程来执行任务,直到线程数达到maximumPoolSize。
- keepAliveTime:线程池线程数超过corePoolSize时,多余线程在多长时间后会被销毁。当线程空闲时间超过keepAliveTime时,这些线程会被回收。
- unit:keepAliveTime的时间单位。可以是TimeUnit的各种常量,如TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分钟)等。
- workQueue:任务队列。用于存放提交的任务,当线程数达到corePoolSize时,新提交的任务会被放入workQueue中。常见的任务队列有ArrayBlockingQueue、LinkedBlockingQueue等。
- threadFactory:线程工厂。用于创建线程,通过自定义线程工厂可以设置线程的名称、优先级等属性。
- handler:任务拒绝策略。当线程数达到maximumPoolSize且workQueue已满时,新提交的任务会被交给handler处理。常见的任务拒绝策略有AbortPolicy(抛出异常)、CallerRunsPolicy(调用者运行策略,由调用线程处理任务)、DiscardPolicy(丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务)。
- HashMap的底层实现原理:
- 数据结构:HashMap底层是数组+链表+红黑树结构。它有一个默认长度为16的数组,每个数组元素是一个链表的头节点。当链表长度超过8且数组长度大于64时,链表会转换为红黑树,以提高查询效率。
- 插入过程:插入元素时,先计算哈希值,通过哈希值找到对应的桶位置(数组下标)。如果桶为空,就直接插入新节点。如果桶不为空,就遍历链表或红黑树找到相同key的节点进行更新,如果找不到相同key的节点,就插入新节点。计算哈希值时,使用key的hashCode方法得到一个哈希值,然后通过扰动函数对哈希值进行处理,使哈希值更加均匀地分布在数组中,减少哈希冲突。
- Spring的核心特性:
- 依赖注入:可以将对象之间的依赖关系通过配置的方式注入。通过依赖注入,对象不需要自己创建依赖对象,而是由容器来创建和注入,降低了对象之间的耦合度。例如,一个类需要依赖另一个类来完成某项功能,通过依赖注入,只需要在配置文件中配置好依赖关系,容器会自动将依赖对象注入到需要的类中。
- 面向切面编程(AOP):可以在不修改原有代码的基础上,动态地添加功能。通过AOP可以将一些通用的功能(如日志记录、事务管理等)从业务逻辑中分离出来,以切面的形式织入到业务逻辑中。比如在一个方法执行前后添加日志记录,只需要定义一个切面,配置好切入点和通知(增强),Spring会自动在方法执行前后执行日志记录的代码。
- IoC容器:负责创建、管理和装配对象。IoC容器通过读取配置文件或注解,创建对象并将它们装配在一起,实现对象之间的依赖关系管理。例如,在Spring配置文件中定义了多个bean,容器会根据配置创建这些bean,并根据依赖关系将它们装配起来,使得各个对象能够协同工作。
- Spring Boot的自动配置原理: Spring Boot通过@EnableAutoConfiguration注解开启自动配置功能。它会根据类路径下的依赖和配置类,自动配置各种组件和属性。当引入了某个依赖时,Spring Boot会自动搜索对应的自动配置类。例如,当引入了Spring Data JPA的依赖,Spring Boot会自动配置JPA相关的组件,如EntityManagerFactory、TransactionManager等。自动配置类会根据一些条件注解(如@Conditional)来决定是否生效。如果满足条件,就会创建相应的bean并进行配置。这样开发者只需要引入依赖,Spring Boot就会自动完成很多配置工作,大大简化了开发过程。
- MyBatis的缓存机制:
- 一级缓存:是SqlSession级别的缓存,同一个SqlSession查询的数据会先从一级缓存中获取,减少数据库查询。当执行一个查询操作时,MyBatis会先在一级缓存中查找,如果找到相同的查询结果,就直接返回缓存中的数据。在同一个SqlSession中,对相同的SQL再次执行查询时,不会再次查询数据库,而是直接使用一级缓存中的数据。当SqlSession关闭时,一级缓存会被清空。
- 二级缓存:是namespace级别的缓存,多个SqlSession可以共享二级缓存。在一个Mapper.xml文件中配置了标签后,就启用了二级缓存。当一个SqlSession查询到数据后,会将数据放入二级缓存中。其他SqlSession在查询相同数据时,如果二级缓存中有数据,就可以直接使用。二级缓存的有效期更长,适用于一些不经常变化的数据。二级缓存会在事务提交或SqlSession关闭时进行刷新。
- Dubbo的服务调用原理:
服务调用时,消费者通过远程代理对象调用服务。代理对象会通过网络向注册中心获取服务提供者的地址列表,然后根据负载均衡策略选择一个提供者进行远程调用。在调用过程中,会进行序列化、反序列化等操作。具体步骤如下:
- 服务注册:服务提供者启动时,将自己提供的服务信息(包括服务接口、实现类、地址等)注册到注册中心。
- 服务发现:消费者启动时,从注册中心获取服务提供者的地址列表。
- 远程调用:消费者通过远程代理对象,根据负载均衡策略选择一个服务提供者地址,发起远程调用。调用过程中,会将调用的方法、参数等信息进行序列化,通过网络传输到服务提供者。
- 结果返回:服务提供者接收到调用请求后,执行相应的方法,将结果进行序列化后返回给消费者。消费者接收到返回结果后,进行反序列化,得到调用结果。
- 负载均衡:Dubbo提供了多种负载均衡策略,如随机、轮询、加权轮询、最少活跃调用数等。随机策略是随机选择一个服务提供者;轮询策略是按照顺序依次调用服务提供者;加权轮询策略是根据服务提供者的性能等因素设置不同的权重,按照权重依次调用;最少活跃调用数策略是选择当前活跃调用数最少的服务提供者。