面试官:请简要介绍一下 Java 核心知识中面向对象的三大特性。
王铁牛:嗯,面向对象的三大特性嘛,就是封装、继承和多态。封装就是把数据和操作数据的方法封装在一起,对外提供统一的接口;继承就是子类继承父类的属性和方法;多态就是同一个方法可以根据对象的不同类型而表现出不同的行为。
面试官:回答得不错。那说说在多线程环境下,如何保证数据的一致性?
王铁牛:可以用 synchronized 关键字来同步代码块或者方法,也可以用 Lock 接口及其实现类来实现锁机制。
面试官:很好。再问一个,JVM 的内存结构分为哪几个部分?
王铁牛:这个……好像是堆、栈、方法区吧。
面试官:第一轮面试结束,回家等通知。
第二轮:
面试官:讲讲线程池的工作原理。
王铁牛:线程池就是预先创建一定数量的线程,当有任务提交时,从线程池中获取线程来执行任务。如果线程池中的线程都在忙,任务就会被放入队列中等待。
面试官:那如何合理配置线程池的参数?
王铁牛:嗯……这个要根据任务的类型和数量来定吧,好像有 corePoolSize、maximumPoolSize 之类的参数。
面试官:HashMap 的底层数据结构是什么?
王铁牛:是数组和链表吧,好像还有红黑树。
面试官:第二轮面试结束,回家等通知。
第三轮:
面试官:说说 Spring 的核心特性。
王铁牛:Spring 可以实现依赖注入、面向切面编程,还有 IoC 容器。
面试官:那 Spring Boot 相对于 Spring 有哪些优势?
王铁牛:Spring Boot 更简单,它可以快速搭建项目,内置了很多依赖,开发起来更方便。
面试官:最后一个问题,Dubbo 的服务调用原理是什么?
王铁牛:这个……不太清楚,好像是通过注册中心来发现服务,然后进行远程调用。
面试官:三轮面试结束,回家等通知。
答案:
- 面向对象的三大特性:
- 封装:将数据和操作数据的方法封装在一起,对外提供统一接口,实现了数据的隐藏和安全访问。例如,一个类中的属性可以用 private 修饰,通过 public 的方法来访问和修改,这样外部类就不能随意修改内部数据,保证了数据的安全性。
- 继承:子类继承父类的属性和方法,实现代码复用。比如一个父类有一些通用的方法,子类可以直接继承使用,避免了重复编写代码。同时子类还可以根据自身需求对继承的方法进行重写,实现不同的行为。
- 多态:同一个方法可以根据对象的不同类型而表现出不同的行为。在程序运行时,根据对象的实际类型来决定调用哪个方法。例如,一个父类类型的引用可以指向子类对象,当调用该引用的某个方法时,实际执行的是子类重写后的方法。
- 多线程环境下保证数据一致性:
- synchronized:可以修饰代码块或者方法。当一个线程访问被 synchronized 修饰的代码块或方法时,会先获取对象的锁,如果锁被其他线程占用,则该线程会等待,直到锁被释放。这样就保证了同一时间只有一个线程能访问被 synchronized 保护的代码,从而保证数据一致性。例如,在一个多线程操作共享资源的场景中,对共享资源的访问方法用 synchronized 修饰,就可以避免数据冲突。
- Lock 接口及其实现类:如 ReentrantLock。Lock 提供了比 synchronized 更灵活的锁控制。可以通过 lock()方法手动获取锁,unlock()方法手动释放锁,还可以通过 tryLock()方法尝试获取锁,避免死锁。例如,在一些复杂的业务逻辑中,需要更精细地控制锁的获取和释放时机,就可以使用 Lock 接口。
- JVM 的内存结构:
- 堆:是 JVM 中最大的一块内存区域,用于存放对象实例。所有的对象实例都在这里分配内存。它是垃圾回收的主要区域,当对象不再被引用时,会被垃圾回收器回收。
- 栈:主要存放局部变量和方法调用的上下文。每个线程都有自己独立的栈空间。栈中的数据随着方法的调用和结束而进栈和出栈。例如,方法中的局部变量会在栈中分配内存。
- 方法区:用于存储已被虚拟机加载的类信息、常量、静态变量等数据。它类似于传统语言中的永久代,但在 Java 8 及以后,方法区被元空间取代,元空间使用本地内存,而不是像永久代那样在 JVM 内存中。
- 线程池的工作原理:线程池预先创建一定数量的线程,当有任务提交时,首先会检查线程池中的线程数量是否小于核心线程数(corePoolSize),如果小于,则创建新线程来执行任务;如果线程数量达到核心线程数,则将任务放入任务队列(BlockingQueue)中;如果任务队列已满,且线程数量小于最大线程数(maximumPoolSize),则创建新线程来执行任务;如果线程数量达到最大线程数,任务会根据拒绝策略进行处理,比如抛出异常、使用调用者所在线程执行等。
- 合理配置线程池的参数:
- corePoolSize:核心线程数,当提交的任务数小于 corePoolSize 时,线程池会创建新线程来执行任务。这个参数要根据任务的性质来设置,如果任务是 CPU 密集型的,corePoolSize 可以设置得大一些,让更多的任务可以并行执行;如果是 I/O 密集型任务,corePoolSize 可以相对小一些,因为 I/O 操作时间长,线程大部分时间处于等待状态,不需要太多线程。
- maximumPoolSize:最大线程数,当任务数超过 corePoolSize 且任务队列已满时,会创建新线程直到线程数达到 maximumPoolSize。这个参数要考虑系统的资源限制,不能设置过大,否则会消耗过多系统资源。
- keepAliveTime:线程池中的线程在空闲时的存活时间。当线程空闲时间超过 keepAliveTime 时,非核心线程会被销毁,以减少资源消耗。
- unit:keepAliveTime 的时间单位。
- workQueue:任务队列,用于存放提交的任务。常见的有 ArrayBlockingQueue、LinkedBlockingQueue 等。不同的任务队列有不同的特性,比如 ArrayBlockingQueue 是有界队列,LinkedBlockingQueue 可以是有界或无界队列,需要根据实际需求选择。
- threadFactory:线程工厂,用于创建线程,通过它可以自定义线程的名称、优先级等属性。
- handler:拒绝策略,当线程池无法处理新任务时,会调用拒绝策略来处理。
- HashMap 的底层数据结构:HashMap 底层是数组和链表/红黑树的结合。在 JDK 1.8 之前,HashMap 的底层是数组 + 链表,当一个 key-value 对插入时,首先计算 key 的哈希值,然后通过哈希值找到对应的数组下标,如果该下标为空,则直接插入新节点;如果不为空,则遍历链表,找到相同 key 的节点进行更新,如果链表长度超过阈值(默认 8),则将链表转换为红黑树,以提高查询效率。在 JDK 1.8 及以后,当链表长度小于等于 6 时,会将红黑树转换回链表,以节省空间。
- Spring 的核心特性:
- 依赖注入(Dependency Injection):通过控制反转(IoC)容器,将对象之间的依赖关系由程序主动创建改为由容器注入。这样可以降低对象之间的耦合度,提高代码的可维护性和可测试性。例如,一个类需要依赖另一个类来完成某个功能,通过依赖注入,容器可以在运行时将所需的依赖对象注入到该类中。
- 面向切面编程(Aspect Oriented Programming,AOP):允许将一些横切关注点(如日志记录、事务管理等)与业务逻辑分离。通过切面(Aspect)、切入点(Pointcut)、通知(Advice)等概念,在不修改原有业务逻辑的基础上,动态地添加额外的功能。比如在多个业务方法中都需要进行日志记录,就可以通过 AOP 来统一实现,而不需要在每个业务方法中都编写日志记录代码。
- IoC 容器:负责创建、配置和管理对象之间的依赖关系。它是 Spring 的核心,通过 XML 配置文件、注解等方式来定义对象的创建方式和依赖关系,容器会根据这些配置来实例化对象并注入依赖。
- Spring Boot 相对于 Spring 的优势:
- 快速搭建项目:Spring Boot 提供了一种快速创建项目的方式,它基于约定大于配置的原则,内置了很多默认配置,使得开发者可以快速搭建一个完整的项目框架,减少了大量的配置文件编写工作。
- 内置依赖:Spring Boot 内置了各种常用的依赖,如 Tomcat、Spring Data JPA 等,开发者不需要手动去添加这些依赖,只需要在项目中引入 Spring Boot 的 Starter 依赖,就可以自动引入相关的依赖包,简化了项目的构建过程。
- 自动配置:Spring Boot 能够根据项目中引入的依赖自动进行配置,例如,如果项目中引入了数据库相关的依赖,Spring Boot 会自动配置好数据源等相关配置,大大减少了开发者的配置工作量,提高了开发效率。
- Actuator:提供了一系列的监控和管理端点,方便开发者对应用程序进行监控和管理,如查看应用的健康状态、性能指标、线程信息等,有助于及时发现和解决问题。
- Dubbo 的服务调用原理:
- 服务注册:服务提供者启动时,会将自己提供的服务信息(包括服务接口、实现类、服务地址等)注册到注册中心。注册中心是 Dubbo 服务治理的核心组件,常用的有 Zookeeper 等。服务提供者通过与注册中心建立连接,将服务信息发送过去并持久化。
- 服务发现:服务消费者启动时,会向注册中心订阅自己需要的服务。注册中心会监听服务提供者的服务信息变化,当有新的服务提供者注册或者已有服务提供者的服务信息发生变化时,注册中心会及时通知服务消费者。
- 远程调用:服务消费者根据从注册中心获取到的服务提供者列表,选择一个合适的服务提供者进行远程调用。Dubbo 支持多种远程调用协议,如 Dubbo 协议、HTTP 协议等。当服务消费者发起调用时,会通过动态代理生成一个代理对象,代理对象封装了远程调用的逻辑,通过网络将调用请求发送到服务提供者。
- 集群容错:当服务消费者调用服务提供者时,如果出现调用失败等情况,Dubbo 提供了集群容错机制。例如,当一个服务提供者调用失败时,会自动切换到其他可用的服务提供者进行重试,以提高调用的成功率。常见的集群容错策略有 failover(失败自动切换)、failfast(快速失败)、failsafe(失败安全,忽略失败)等。
- 负载均衡:在服务消费者选择服务提供者时,Dubbo 支持多种负载均衡算法,如随机负载均衡、轮询负载均衡、加权轮询负载均衡等。通过负载均衡算法,服务消费者可以从多个服务提供者中选择一个合适的进行调用,以实现资源的合理分配和调用性能的优化。