《互联网大厂Java面试:核心知识大考验》

29 阅读11分钟

互联网大厂Java面试:核心知识大考验

面试官:请简要介绍一下Java核心知识中,面向对象编程的三大特性。

王铁牛:这简单,是封装、继承和多态。封装就是把对象的属性和方法包装起来,对外提供统一的接口;继承是子类继承父类的属性和方法;多态就是同一个方法可以根据对象的不同类型表现出不同的行为。

面试官:不错,回答得很准确。那在多线程编程中,如何确保线程安全?

王铁牛:可以使用synchronized关键字来同步代码块或方法,也可以用Lock接口及其实现类,比如ReentrantLock。还可以使用线程安全的集合类,像ConcurrentHashMap。

面试官:嗯,回答得比较全面。接下来问你几个关于JUC的问题,CountDownLatch和CyclicBarrier有什么区别?

王铁牛:这个……CountDownLatch是一个线程等待其他线程完成任务后再执行,它的计数器是递减的,减到0就释放等待的线程。CyclicBarrier是多个线程互相等待,直到所有线程都到达某个屏障点,然后一起继续执行,它的计数器是可以循环使用的。

面试官:好,第一轮提问结束。接下来进入第二轮。请说说JVM的内存结构。

王铁牛:JVM内存结构包括堆、栈、方法区、程序计数器、本地方法栈。堆是存放对象实例的地方;栈是存放局部变量和方法调用的地方;方法区存储类信息、常量、静态变量等;程序计数器记录当前线程执行的字节码指令地址;本地方法栈用于执行本地方法。

面试官:那类加载机制的过程是怎样的?

王铁牛:类加载机制分为加载、验证、准备、解析、初始化五个阶段。加载就是把类的字节码文件读入内存;验证确保字节码文件的正确性;准备为类的静态变量分配内存并赋初始值;解析把符号引用转换为直接引用;初始化执行类的静态代码块和为静态变量赋值。

面试官:在多线程环境下,如何优化线程池的使用?

王铁牛:嗯……可以根据任务的类型和数量来合理配置线程池的参数,比如核心线程数、最大线程数、队列容量等。对于CPU密集型任务,可以适当减少线程池的线程数;对于I/O密集型任务,可以增加线程池的线程数。

面试官:第二轮提问完毕。现在进行第三轮。讲讲HashMap的底层实现原理。

王铁牛:HashMap底层是基于数组和链表实现的。当往HashMap中插入键值对时,会先计算键的哈希值,然后通过哈希值找到对应的数组位置。如果该位置为空,就直接插入新的键值对;如果不为空,就会遍历链表或红黑树,找到相同键的节点并更新其值,如果链表长度超过阈值,就会将链表转换为红黑树。

面试官:那ArrayList的扩容机制是怎样的?

王铁牛:ArrayList扩容时,会创建一个新的数组,新数组的容量是原数组容量的1.5倍(如果原数组容量小于10)或者原数组容量加上原数组容量的一半(如果原数组容量大于等于10)。然后将原数组的元素复制到新数组中。

面试官:说说Spring框架中IoC和AOP的概念。

王铁牛:IoC就是控制反转,它把对象的创建和依赖关系的管理交给Spring容器,而不是由应用程序自己来创建和管理。AOP是面向切面编程,它允许开发者将一些横切关注点,比如日志记录、事务管理等,从业务逻辑中分离出来,以提高代码的可维护性和复用性。

面试官:好,三轮提问都结束了。回去等通知吧。

答案

  1. 面向对象编程的三大特性
    • 封装:将对象的属性和方法包装起来,对外提供统一的接口。这样可以隐藏对象的内部实现细节,提高代码的安全性和可维护性。例如,在一个类中,将一些属性设置为private,通过public的方法来访问和修改这些属性。
    • 继承:子类继承父类的属性和方法。通过继承,可以实现代码的复用,减少重复代码。比如,创建一个父类“Animal”,子类“Dog”和“Cat”可以继承“Animal”的属性(如颜色、体重等)和方法(如eat())。
    • 多态:同一个方法可以根据对象的不同类型表现出不同的行为。这使得程序更加灵活和可扩展。例如,定义一个父类“Shape”,有子类“Circle”和“Rectangle”,它们都有draw()方法,但实现不同,当调用draw()方法时,根据对象是“Circle”还是“Rectangle”会表现出不同的绘制行为。
  2. 确保线程安全的方法
    • synchronized关键字
      • 可以同步代码块,如synchronized (object) { // 代码 },确保在同一时刻只有一个线程能进入该代码块。
      • 也可以同步方法,如public synchronized void method() { // 方法体 },保证同一时刻只有一个线程能调用该方法。
    • Lock接口及其实现类(如ReentrantLock)
      • 使用Lock lock = new ReentrantLock();获取锁对象。
      • 通过lock.lock()方法获取锁,lock.unlock()方法释放锁。相比synchronized,它提供了更灵活的锁控制,比如可中断锁、定时锁等。
    • 线程安全的集合类(如ConcurrentHashMap):它在多线程环境下能保证数据的一致性和线程安全。在插入、读取和修改数据时,通过内部的分段锁机制,减少锁的竞争,提高并发性能。
  3. CountDownLatch和CyclicBarrier的区别
    • CountDownLatch
      • 一个线程等待其他线程完成任务后再执行。
      • 计数器是递减的,比如创建一个CountDownLatch(3),当其他三个线程完成任务后,调用countDown()方法使计数器减1,当计数器减到0时,等待的线程被释放。
    • CyclicBarrier
      • 多个线程互相等待,直到所有线程都到达某个屏障点,然后一起继续执行。
      • 计数器可以循环使用,例如创建一个CyclicBarrier(3),三个线程调用await()方法等待,当三个线程都到达时,一起冲破屏障继续执行,之后计数器又可以重新开始计数。
  4. JVM的内存结构
    • :是存放对象实例的地方,是JVM内存中最大的一块区域。对象在堆中分配内存,垃圾回收主要针对堆进行。
    • :存放局部变量和方法调用的地方。每个线程都有自己独立的栈空间,栈帧用于存储方法调用时的局部变量、操作数栈等信息。
    • 方法区:存储类信息、常量、静态变量等。在Java 8及以后,方法区被元空间取代,元空间使用本地内存,而不是像之前方法区那样在JVM内存中。
    • 程序计数器:记录当前线程执行的字节码指令地址。它是线程私有的,每个线程都有自己的程序计数器。
    • 本地方法栈:用于执行本地方法(用C或C++实现的方法)。
  5. 类加载机制的过程
    • 加载:把类的字节码文件读入内存,创建一个类的Class对象。可以通过类加载器来完成加载,比如启动类加载器、扩展类加载器、应用类加载器等。
    • 验证:确保字节码文件的正确性,检查字节码是否符合Java虚拟机规范,防止恶意字节码的注入。
    • 准备:为类的静态变量分配内存并赋初始值。例如,静态变量int a = 10;,在准备阶段会为a分配内存并初始化为0。
    • 解析:把符号引用转换为直接引用。符号引用是指在编译时使用的类名、方法名等,解析阶段会将其转换为在内存中的实际地址(直接引用)。
    • 初始化:执行类的静态代码块和为静态变量赋值。当初始化一个类时,会先初始化它的父类,按照从上到下的顺序执行静态代码块。
  6. 优化线程池的使用
    • 根据任务类型配置参数
      • CPU密集型任务:由于任务主要消耗CPU资源,线程数不宜过多,可适当减少线程池的核心线程数。比如根据CPU核心数来估算,一般可以设置核心线程数为CPU核心数 + 1。因为过多的线程会导致频繁的线程上下文切换,反而降低性能。
      • I/O密集型任务:这类任务主要等待I/O操作,线程数可以适当增加。可以根据任务的I/O等待时间和CPU计算时间的比例来估算线程数。例如,如果任务大部分时间在等待I/O,核心线程数可以设置为CPU核心数的2 - 3倍,以充分利用I/O资源,提高整体性能。
    • 合理设置队列容量:如果任务提交速度大于线程处理速度,队列可以起到缓冲作用。队列容量的设置要根据任务的数量和预计的处理时间来确定。如果队列过大,可能会消耗过多内存;队列过小,可能会导致任务堆积,甚至拒绝新任务。
  7. HashMap的底层实现原理
    • 基于数组和链表(或红黑树)
      • 当往HashMap中插入键值对时,首先计算键的哈希值。通过哈希算法将键映射为一个整数作为哈希值。
      • 根据哈希值找到对应的数组位置。如果该位置为空,就直接插入新的键值对。
      • 如果该位置不为空,就会遍历链表(JDK 1.8之前)或红黑树(JDK 1.8及以后)。当链表长度超过阈值(默认8)时,链表会转换为红黑树,以提高查询效率。找到相同键的节点并更新其值。
    • 哈希冲突解决:通过链地址法来解决哈希冲突,即将哈希值相同的键值对存储在同一个链表或红黑树中。在插入和查找时,通过遍历链表或红黑树来找到对应的键值对。
  8. ArrayList的扩容机制
    • 当ArrayList的容量不足时,会进行扩容。
    • 扩容时,会创建一个新的数组,新数组的容量是原数组容量的1.5倍(如果原数组容量小于10)或者原数组容量加上原数组容量的一半(如果原数组容量大于等于10)。
    • 然后将原数组的元素复制到新数组中。这样就完成了扩容操作,保证了ArrayList能够动态地增加容量以存储更多元素。
  9. Spring框架中IoC和AOP的概念
    • IoC(控制反转)
      • 把对象的创建和依赖关系的管理交给Spring容器。传统方式下,对象之间的依赖关系由应用程序自己负责创建和维护,而IoC将这个职责交给了Spring容器。
      • 例如,一个类A依赖于类B,在IoC模式下,Spring容器会负责创建类B,并将其注入到类A中。这样应用程序只需要使用对象,而不需要关心对象的创建过程,降低了对象之间的耦合度,提高了代码的可维护性和可测试性。
    • AOP(面向切面编程)
      • 允许开发者将一些横切关注点,比如日志记录、事务管理等,从业务逻辑中分离出来。
      • 例如,在一个电商系统中,订单处理的业务逻辑是主要关注点,而日志记录和事务管理就是横切关注点。通过AOP,可以将日志记录和事务管理的代码独立出来,织入到订单处理的业务逻辑中,而不需要在每个业务方法中重复编写这些代码,提高了代码的复用性和可维护性。