面试官:好,咱们开始面试。第一轮,先问几个Java核心知识的问题。首先,说说Java中的多态是怎么实现的?
王铁牛:多态就是一个对象可以表现出多种形态嘛。通过方法重写和接口实现就能实现多态。
面试官:嗯,回答得挺简洁明了。那再问个问题,String类为什么是不可变的?
王铁牛:因为String类内部用final修饰了字符数组,一旦初始化就不能再修改了。
面试官:不错,这两个问题回答得都还可以。接下来问几个关于JUC的问题。在并发编程中,CountDownLatch和CyclicBarrier有什么区别?
王铁牛:CountDownLatch是一个线程等待其他线程完成任务后再执行,CyclicBarrier是多个线程相互等待,直到所有线程都到达某个点后再一起执行。
面试官:嗯,回答得基本正确。第一轮面试就到这里,整体表现还不错。
第二轮面试开始。首先,讲讲JVM的内存模型都有哪些部分?
王铁牛:JVM内存模型有堆、栈、方法区、本地方法栈、程序计数器这些。
面试官:那方法区和堆有什么区别?
王铁牛:方法区主要存类信息、常量、静态变量啥的,堆主要存对象实例。
面试官:再问个多线程的问题,如何在Java中实现线程安全的单例模式?
王铁牛:可以用双重检查锁或者静态内部类的方式。
面试官:第二轮面试也结束了,有些问题回答得还行,但有些回答得不是特别清晰。
第三轮面试。先问线程池相关的,ThreadPoolExecutor的几个参数分别代表什么含义?
王铁牛:corePoolSize是核心线程数,maximumPoolSize是最大线程数,keepAliveTime是线程空闲时的存活时间,unit是时间单位,workQueue是任务队列,handler是任务拒绝策略。
面试官:说说HashMap的底层实现原理。
王铁牛:它是数组加链表再加红黑树,通过哈希值找到对应的桶位置,链表长度超过8且数组长度大于64时会转换为红黑树。
面试官:最后问个Spring的问题,Spring的核心特性有哪些?
王铁牛:控制反转、依赖注入啥的。
面试官:好了,三轮面试都结束了。你整体有些问题回答得还可以,但部分复杂问题回答得不是很清晰。回去等通知吧。
答案:
- Java中的多态是怎么实现的:
- 方法重写:在子类中重新定义父类的方法,当通过子类对象调用该方法时,会执行子类重写后的方法,这体现了多态。例如,父类有一个方法
void print(),子类重写为void print() { System.out.println("子类的print方法"); },当子类对象调用print方法时,就会执行子类重写后的方法。 - 接口实现:一个类实现一个或多个接口,当使用接口类型的变量引用实现该接口的类的对象时,通过该变量调用接口中的方法,会执行实现类的具体实现,这也是多态的一种体现。比如接口
MyInterface有方法void doSomething(),类MyClass实现了该接口,当MyInterface my = new MyClass();,调用my.doSomething()时,实际执行的是MyClass中的实现。
- 方法重写:在子类中重新定义父类的方法,当通过子类对象调用该方法时,会执行子类重写后的方法,这体现了多态。例如,父类有一个方法
- String类为什么是不可变的:
- String类内部使用
final修饰字符数组value,即private final char value[];。 - 当一个String对象被创建后,其内部的字符数组就不能再被修改。例如,当调用
str = "new value";时,实际上是创建了一个新的String对象,而不是修改原来对象的字符数组。这样保证了String对象的安全性和稳定性,多个引用可以共享同一个String对象,不用担心其值被意外修改。
- String类内部使用
- CountDownLatch和CyclicBarrier有什么区别:
- CountDownLatch:
- 它允许一个或多个线程等待其他线程完成一系列操作后再继续执行。
- 它通过构造函数传入一个计数值,当调用
countDown()方法时,计数值减1,当计数值为0时,等待的线程会被唤醒继续执行。例如,有一个主线程需要等待3个子线程完成任务后再执行后续操作,就可以创建一个CountDownLatch(3),在子线程任务完成时调用countDown(),主线程调用await()方法等待。
- CyclicBarrier:
- 它是让一组线程相互等待,直到所有线程都到达某个屏障点后再一起继续执行。
- 它通过构造函数传入一个计数值和一个可选的屏障动作。当线程调用
await()方法时,会等待其他线程也调用await()方法,当所有线程都调用了await()方法,计数值达到0时,所有线程会被释放,并可以选择执行传入的屏障动作。例如,多个线程需要共同完成一个计算任务,每个线程计算一部分,当所有线程都计算完成后,一起进行汇总操作,就可以使用CyclicBarrier。
- CountDownLatch:
- JVM的内存模型都有哪些部分:
- 堆(Heap):是JVM中最大的一块内存区域,用于存储对象实例和数组。所有的对象实例都在这里分配内存。堆可以被细分为新生代、老年代等不同区域,不同区域有不同的垃圾回收策略。
- 栈(Stack):每个线程都有自己独立的栈空间,用于存储局部变量、方法调用等。栈中的数据遵循先进后出的原则。
- 方法区(Method Area):存储类信息、常量、静态变量等数据。在Java 8及以后,方法区被称为元空间(MetaSpace),它不再在JVM的堆中,而是使用本地内存。
- 本地方法栈(Native Method Stack):与栈类似,用于执行本地方法(用C或C++实现的方法)。
- 程序计数器(Program Counter Register):记录当前线程执行的字节码指令地址,是线程私有的。
- 方法区和堆有什么区别:
- 存储内容不同:
- 方法区:主要存储类的元数据信息,如类的结构、字段、方法等定义,以及常量、静态变量等。例如,类的全限定名、父类信息、接口信息、字段表、方法表等都存放在方法区。
- 堆:主要存储对象实例,包括对象的成员变量值等。当创建一个对象时,会在堆中分配内存空间来存储该对象的具体数据。
- 内存管理方式不同:
- 方法区:其内存回收主要针对常量池的回收和类型的卸载。当一个类的所有实例都被回收,并且该类的类加载器被回收后,这个类就可以被卸载,对应的方法区内存也会被回收。
- 堆:是垃圾回收的主要区域,有多种垃圾回收算法来管理堆内存,如标记清除、标记整理、复制算法等,根据不同的区域特点选择合适的算法进行对象的回收和内存的整理。
- 存储内容不同:
- 如何在Java中实现线程安全的单例模式:
- 双重检查锁(Double-Checked Locking):
- 代码示例:
- 双重检查锁(Double-Checked Locking):
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
- 解释:首先通过`volatile`关键字修饰`instance`变量,保证其可见性。当多个线程同时调用`getInstance`方法时,首先检查`instance`是否为`null`,如果是,则进入同步块。在同步块中再次检查`instance`是否为`null`,如果还是`null`,才创建实例。这样可以避免不必要的同步开销,只有在实例未创建时才进行同步创建操作。
- 静态内部类(Static Inner Class):
- 代码示例:
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
- 解释:静态内部类`SingletonHolder`只有在调用`getInstance`方法时才会被加载,加载时会创建`Singleton`的实例。由于类的加载机制是线程安全的,所以这种方式天然保证了线程安全,并且只有在真正需要实例时才会创建,实现了懒加载。
7. ThreadPoolExecutor的几个参数分别代表什么含义:
- corePoolSize(核心线程数):线程池创建时初始化的线程数,当提交的任务数小于
corePoolSize时,线程池会创建新的线程来执行任务。 - maximumPoolSize(最大线程数):线程池允许的最大线程数。当提交的任务数大于
corePoolSize且任务队列已满时,会创建新的线程,直到线程数达到maximumPoolSize。 - keepAliveTime(线程空闲时的存活时间):当线程数大于
corePoolSize时,多余的线程在空闲一段时间后会被销毁,这个空闲时间就是由keepAliveTime指定的。 - unit(时间单位):
keepAliveTime的时间单位,如TimeUnit.SECONDS表示秒。 - workQueue(任务队列):用于存放提交的任务,当提交的任务数大于
corePoolSize时,会将任务放入任务队列中。常见的任务队列有ArrayBlockingQueue、LinkedBlockingQueue等。 - handler(任务拒绝策略):当线程数达到
maximumPoolSize且任务队列已满时,会调用任务拒绝策略来处理新提交的任务。常见的任务拒绝策略有AbortPolicy(抛出异常)、CallerRunsPolicy(调用者运行任务)、DiscardPolicy(丢弃任务)、DiscardOldestPolicy(丢弃队列中最旧的任务)。
- HashMap的底层实现原理:
- 数组加链表再加红黑树:
- HashMap内部维护一个数组,数组的每个元素是一个链表的头节点(在Java 8之前只有链表,Java 8及以后链表长度超过8且数组长度大于64时会转换为红黑树)。
- 当插入一个键值对时,首先通过哈希函数计算键的哈希值,然后根据哈希值找到对应的数组索引位置。
- 如果该位置为空,则直接插入新节点。
- 如果该位置不为空,则遍历链表(或红黑树),如果找到相同的键,则更新其值;如果未找到相同的键,则在链表(或红黑树)末尾插入新节点。
- 当链表长度超过8且数组长度大于64时,链表会转换为红黑树,以提高查找效率。红黑树是一种自平衡二叉查找树,插入、删除和查找操作的时间复杂度都是O(log n),相比链表的O(n)效率更高。
- 数组加链表再加红黑树:
- Spring的核心特性有哪些:
- 控制反转(IoC,Inversion of Control):
- 传统的软件开发中,对象之间的依赖关系由对象自身创建和管理。而在Spring中,控制权被反转,由Spring容器来创建和管理对象之间的依赖关系。例如,一个类A依赖类B,在Spring中,不是在类A中直接创建类B的实例,而是由Spring容器根据配置创建类B的实例并注入到类A中。
- 依赖注入(DI,Dependency Injection):
- 这是实现控制反转的一种方式,通过构造函数注入、setter方法注入等方式将依赖对象注入到目标对象中。比如,通过构造函数注入:
public class A { private B b; public A(B b) { this.b = b; } },Spring容器会根据配置创建B的实例并传入A的构造函数。
- 这是实现控制反转的一种方式,通过构造函数注入、setter方法注入等方式将依赖对象注入到目标对象中。比如,通过构造函数注入:
- 面向切面编程(AOP,Aspect-Oriented Programming):
- 它允许将一些横切关注点(如日志记录、事务管理等)与业务逻辑分离。通过定义切面、切点和通知,在不修改业务逻辑代码的情况下,为业务方法添加额外的功能。例如,可以定义一个日志切面,在特定的业务方法执行前后记录日志。
- IoC容器:
- Spring提供了一个IoC容器来管理对象及其依赖关系。容器负责创建、配置和组装对象,根据配置文件(如XML或注解配置)来决定创建哪些对象以及如何注入它们的依赖。
- 事务管理:
- Spring提供了强大的事务管理功能,支持声明式事务和编程式事务。声明式事务可以通过注解(如
@Transactional)或XML配置来定义事务规则,极大地简化了事务管理的代码。例如,在一个业务方法上添加@Transactional注解,就可以使其具有事务特性,保证数据的一致性。
- Spring提供了强大的事务管理功能,支持声明式事务和编程式事务。声明式事务可以通过注解(如
- 控制反转(IoC,Inversion of Control):