互联网大厂Java面试大揭秘:核心知识与实际应用的碰撞
面试官:请简要介绍一下Java中的多线程机制,以及在实际业务场景中如何避免死锁?
王铁牛:多线程就是一个程序里可以同时运行多个线程。避免死锁嘛,就是要避免多个线程互相等待对方释放资源,比如按照相同顺序获取锁之类的。
面试官:回答得不错。那再说说线程池的原理和优势,以及如何合理配置线程池参数?
王铁牛:线程池就是预先创建一些线程,当有任务来的时候就从里面拿线程去执行。优势是减少线程创建和销毁的开销。配置参数嘛,核心线程数、最大线程数这些得根据任务类型和数量来定。
面试官:嗯,理解得挺到位。最后问一下,在高并发场景下,如何保证线程安全地操作共享资源?
王铁牛:可以用锁啊,像synchronized关键字或者Lock接口。
第一轮结束,王铁牛对简单问题回答得较为清晰,面试官给予了肯定。
面试官:请详细讲讲JVM的内存模型,以及各个区域的作用。
王铁牛:JVM内存模型包括堆、栈、方法区等。堆用来存对象,栈存局部变量,方法区存类信息啥的。
面试官:那类加载机制的过程是怎样的?
王铁牛:类加载就是把类的字节码文件加载到内存里,有加载、验证、准备、解析、初始化这些步骤。
面试官:如何排查JVM内存泄漏问题?
王铁牛:可以用工具查看内存使用情况,看看有没有对象一直不被回收。
第二轮结束,王铁牛部分回答较模糊,面试官没有过多评价。
面试官:说说Spring框架的核心特性和优势。
王铁牛:Spring能做依赖注入、面向切面编程啥的,优势就是方便开发,提高代码可维护性。
面试官:那Spring Boot与传统Spring相比,有哪些改进?
王铁牛:Spring Boot更简单,自动配置很多东西,能快速搭建项目。
面试官:在Spring Boot项目中,如何实现自定义配置?
王铁牛:可以创建配置类,用注解来配置。
第三轮结束,王铁牛回答得好坏参半。
面试结束,面试官表示会让王铁牛回家等通知。整体来看,王铁牛在一些基础知识上有一定了解,但复杂问题的回答存在不足,需要进一步提升对Java核心知识的深入理解和实际应用能力。
答案
- Java多线程机制及避免死锁:
- 多线程机制:Java中的多线程是指一个程序可以同时运行多个线程。每个线程都有自己独立的执行路径,可以并发执行,提高程序的执行效率。Java通过Thread类和Runnable接口来创建和管理线程。例如,继承Thread类并重写run方法,或者实现Runnable接口的run方法,然后将实现类对象作为参数传递给Thread类的构造函数来创建线程。
- 避免死锁:死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。要避免死锁,可以采取以下措施:
- 破坏互斥条件:互斥条件是指进程对所分配到的资源进行排他性使用,即在一段时间内某资源只由一个进程占用。如果破坏这个条件,就可以避免死锁。但在大多数情况下,资源的排他性使用是必要的,所以这种方法不太可行。
- 破坏请求和保持条件:请求和保持条件是指进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。可以让进程在请求新资源时,先释放已占有的资源,等获取到新资源后再重新申请。
- 破坏不剥夺条件:不剥夺条件是指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。可以允许进程在必要时剥夺其他进程占有的资源。
- 破坏环路等待条件:环路等待条件是指在发生死锁时,进程资源图必定构成环,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。可以对资源进行编号,每个进程只能按照编号递增的顺序请求资源,这样就不会形成环路。
- 线程池的原理和优势及合理配置参数:
- 原理:线程池预先创建一定数量的线程,当有任务提交时,从线程池中获取线程来执行任务。线程池维护着多个线程,这些线程不断地从任务队列中获取任务并执行。当任务队列为空时,线程不会被销毁,而是处于等待状态,直到有新任务到来。
- 优势:
- 减少线程创建和销毁的开销:创建和销毁线程是比较耗费资源的操作,线程池可以复用已有的线程,避免频繁创建和销毁线程,提高系统性能。
- 提高响应速度:当有任务到达时,可以直接从线程池中获取线程执行,而不需要等待线程创建,从而提高任务的响应速度。
- 便于管理:线程池可以统一管理线程,比如设置线程的优先级、监控线程的状态等。
- 合理配置参数:
- corePoolSize(核心线程数):线程池的基本大小,当提交的任务数小于corePoolSize时,线程池会创建新线程来执行任务。
- maximumPoolSize(最大线程数):线程池允许的最大线程数,当提交的任务数大于corePoolSize且任务队列已满时,会创建新线程,直到线程数达到maximumPoolSize。
- keepAliveTime(线程存活时间):当线程数大于corePoolSize时,多余的线程在空闲时会存活的时间。
- unit(时间单位):keepAliveTime的时间单位。
- workQueue(任务队列):用于存放提交的任务,当线程池的线程都在忙碌时,任务会被放入任务队列中等待执行。常见的任务队列有ArrayBlockingQueue、LinkedBlockingQueue等。
- 高并发场景下保证线程安全操作共享资源:
- 使用synchronized关键字:
- 同步代码块:可以使用synchronized关键字来修饰一个代码块,在代码块执行期间,其他线程无法进入该代码块,从而保证共享资源的安全。例如:
public class SynchronizedExample { private int count = 0; public void increment() { synchronized (this) { count++; } } } - 同步方法:也可以使用synchronized关键字修饰方法,这样整个方法在执行期间都是同步的。例如:
public class SynchronizedExample { private int count = 0; public synchronized void increment() { count++; } }
- 同步代码块:可以使用synchronized关键字来修饰一个代码块,在代码块执行期间,其他线程无法进入该代码块,从而保证共享资源的安全。例如:
- 使用Lock接口:
- ReentrantLock:是Lock接口的一个实现类,提供了比synchronized更灵活的锁控制。例如:
import java.util.concurrent.locks.ReentrantLock; public class LockExample { private int count = 0; private ReentrantLock lock = new ReentrantLock(); public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } } } - 读写锁:ReadWriteLock接口有两个实现类,分别是ReentrantReadWriteLock。适用于读多写少的场景,允许多个线程同时读,但写操作时会独占锁。
- ReentrantLock:是Lock接口的一个实现类,提供了比synchronized更灵活的锁控制。例如:
- 使用synchronized关键字:
- JVM内存模型及各区域作用:
- JVM内存模型包括:
- 堆(Heap):是JVM中最大的一块内存区域,用于存放对象实例。所有的对象实例都在堆中分配内存。堆被分为新生代、老年代和永久代(Java 8之后为元空间)。新生代又分为Eden区和两个Survivor区。新创建的对象一般会在Eden区分配内存,当Eden区满了,会触发Minor GC,将存活的对象复制到Survivor区,经过多次Minor GC后,还存活的对象会被晋升到老年代。
- 栈(Stack):每个线程都有自己独立的栈,用于存放局部变量、方法调用等。栈中的数据是线程私有的,生命周期与线程相同。当一个方法被调用时,会在栈中创建一个栈帧,用于存储该方法的局部变量和操作数。
- 方法区(Method Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在Java 8之前,方法区被称为永久代,容易出现内存溢出问题,Java 8之后将其改为元空间,元空间使用的是本地内存。
- 程序计数器(Program Counter Register):是一块较小的内存空间,它记录了当前线程正在执行的字节码指令的地址。程序计数器是线程私有的,每个线程都有自己独立的程序计数器。
- JVM内存模型包括:
- 类加载机制的过程:
- 加载:查找并加载类的二进制字节流,将其读入内存,并创建一个java.lang.Class对象来表示这个类。加载过程可以通过类加载器来完成,Java中有多种类加载器,如启动类加载器、扩展类加载器、应用程序类加载器等。
- 验证:验证加载进来的字节流是否符合JVM规范,是否包含正确的类结构信息等,以确保代码的安全性。
- 准备:为类的静态变量分配内存,并设置默认初始值。例如,对于int类型的静态变量,默认初始值为0。
- 解析:将类的二进制字节流中的符号引用替换为直接引用。符号引用是指在编译时使用的类名、方法名等,而直接引用是指指向实际内存地址的引用。
- 初始化:执行类的静态代码块、为静态变量赋值等操作,完成类的初始化。在这个阶段,会按照代码的顺序执行静态变量的赋值和静态代码块中的代码。
- 排查JVM内存泄漏问题:
- 使用工具查看内存使用情况:
- jmap:可以生成Java应用程序的堆转储快照(heap dump),通过分析堆转储快照,可以查看对象的数量、大小、内存占用情况等信息,找出可能存在内存泄漏的对象。例如,使用命令“jmap -dump:format=b,file=heapdump.hprof ”来生成堆转储快照,然后可以使用MAT(Memory Analyzer Tool)等工具来分析该快照。
- jstat:用于监视JVM的各种运行时统计信息,如类加载、内存使用、垃圾回收等情况。可以通过它来实时观察内存的变化趋势,判断是否存在内存泄漏。例如,使用命令“jstat -gc ”来查看垃圾回收情况,其中表示间隔时间(毫秒),表示执行次数。
- 分析对象引用关系:通过MAT等工具分析堆转储快照,查看对象之间的引用关系,找出是否存在一些对象的生命周期过长,导致无法被垃圾回收。如果发现某个对象被其他对象强引用,且这些引用链无法被断开,那么这个对象可能就是导致内存泄漏的原因。例如,如果一个对象被一个静态集合强引用,且这个静态集合在程序运行期间一直存在,那么这个对象就不会被垃圾回收。
- 使用工具查看内存使用情况:
- Spring框架的核心特性和优势:
- 依赖注入(Dependency Injection):通过控制反转(IoC)容器,将对象之间的依赖关系由程序代码直接控制转变为由容器来管理。这样可以降低对象之间的耦合度,提高代码的可维护性和可测试性。例如,在一个类中需要依赖另一个类的实例时,可以通过容器将该依赖注入进来,而不是在类内部直接创建依赖对象。
- 面向切面编程(Aspect - Oriented Programming,AOP):允许开发者将一些横切关注点(如日志记录、事务管理等)从业务逻辑中分离出来,以提高代码的模块化和可复用性。通过定义切面(Aspect),可以在不修改原有业务逻辑的基础上,添加额外的功能。例如,使用AOP可以在方法执行前后添加日志记录,或者在方法出现异常时进行事务回滚。
- IoC容器:负责创建、配置和管理对象及其依赖关系。IoC容器提供了一种方式来集中管理对象的创建和依赖注入,使得对象的创建和使用更加清晰和可控。
- 事务管理:Spring提供了强大的事务管理功能,支持声明式事务和编程式事务。声明式事务可以通过注解或XML配置来定义事务规则,使得开发者可以更方便地管理事务。例如,使用@Transactional注解可以很容易地为一个方法添加事务支持,当方法执行出现异常时,事务会自动回滚。
- Spring Boot与传统Spring相比的改进:
- 自动配置:Spring Boot提供了大量的自动配置功能,能够根据项目的依赖自动配置各种组件和功能。例如,如果你在项目中引入了Spring Data JPA依赖,Spring Boot会自动配置好数据源、JPA相关的配置等,大大减少了开发者的配置工作量。
- 简化项目搭建:传统Spring项目需要进行大量的XML配置来搭建项目结构和配置各种组件。而Spring Boot通过约定大于配置的原则,使得项目的搭建更加简单快捷。可以通过创建一个Spring Boot项目的初始工程,然后根据需求添加依赖和编写代码,就可以快速搭建起一个完整的项目。
- 内置Web服务器:Spring Boot内置了Tomcat等Web服务器,不需要像传统Spring项目那样手动配置和部署Web服务器。这使得项目的部署更加方便,只需要将打包后的jar包直接运行即可。
- Actuator监控:Spring Boot提供了Actuator功能,用于监控和管理应用程序的运行状态。可以通过Actuator端点来获取应用程序的各种信息,如健康状态、内存使用情况、线程信息等,方便进行运维和故障排查。
- 在Spring Boot项目中实现自定义配置:
- 创建配置类:使用Java代码创建一个配置类,并在类上添加@Configuration注解,表示这是一个配置类。例如:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MyConfig { @Bean public MyService myService() { return new MyService(); } } - 使用注解配置:可以在配置类中使用各种注解来配置Bean的属性和行为。例如,使用@Value注解来注入属性值:
import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MyConfig { @Value("${my.property.value}") private String myProperty; @Bean public MyService myService() { MyService service = new MyService(); service.setMyProperty(myProperty); return service; } } - 外部配置文件:可以将配置信息放在外部配置文件中,如application.properties或application.yml文件。在配置类中通过@Value注解读取这些配置文件中的值。例如,在application.properties文件中添加“my.property.value=someValue”,然后在配置类中使用@Value("${my.property.value}")来获取该值。
- 创建配置类:使用Java代码创建一个配置类,并在类上添加@Configuration注解,表示这是一个配置类。例如: