面试官:请简要介绍一下 Java 核心知识中,面向对象的三大特性。
王铁牛:嗯,面向对象三大特性嘛,封装、继承、多态。封装就是把数据和操作数据的方法封装在一起;继承就是子类继承父类的属性和方法;多态就是同一个行为具有多个不同表现形式。
面试官:不错,回答得很准确。那说说 JUC 包下常用的几个类及其作用。
王铁牛:JUC 包下啊,像 CountDownLatch 可以实现线程间的同步,CyclicBarrier 能让一组线程互相等待,直到到达某个公共屏障点。还有 Semaphore 用于控制同时访问某个资源的线程个数。
面试官:好,第一轮表现不错。接下来问几个 JVM 相关的问题。类加载的过程分为哪几个阶段?
王铁牛:这个,嗯,好像是加载、验证、准备、解析、初始化吧。
面试官:那说说方法区和堆内存的区别。
王铁牛:方法区主要存放类信息、常量、静态变量等,堆内存是对象实例的存储区域。
面试官:再问一个,JVM 中如何进行垃圾回收的?
王铁牛:这个,就是通过一些算法,像标记清除、标记整理、复制算法这些来回收垃圾对象。
面试官:第二轮结束。下面进入多线程和线程池的问题。首先,创建线程有哪几种方式?
王铁牛:有继承 Thread 类、实现 Runnable 接口、实现 Callable 接口这几种方式。
面试官:那说说线程池的几个重要参数及其含义。
王铁牛:corePoolSize 是核心线程数,maximumPoolSize 是最大线程数,keepAliveTime 是线程存活时间,unit 是时间单位,workQueue 是任务队列。
面试官:当提交的任务数大于线程池最大线程数时会怎样?
王铁牛:嗯,这个,会把任务放到任务队列里,如果队列满了,就根据拒绝策略来处理。
面试官:面试暂时到这里,回去等通知吧。
答案:
- 面向对象三大特性:
- 封装:将数据和操作数据的方法绑定在一起,对外提供统一的访问接口,隐藏内部实现细节,提高数据的安全性和程序的可维护性。例如一个类中有私有成员变量和公共的访问方法。
- 继承:子类继承父类的属性和方法,实现代码复用。比如一个子类可以继承父类的一些通用方法,减少代码冗余。
- 多态:同一个行为具有多个不同表现形式。分为编译时多态(方法重载)和运行时多态(方法重写)。通过多态可以根据对象的实际类型来调用相应的方法。
- JUC 包下常用类及其作用:
- CountDownLatch:用于协调多个线程之间的同步。比如多个线程需要等待某个条件满足后再同时执行,可以使用 CountDownLatch 来实现。通过调用 countDown 方法减少计数,当计数为 0 时,等待的线程会被唤醒。
- CyclicBarrier:让一组线程互相等待,直到到达某个公共屏障点。例如多个线程共同完成一个任务,每个线程完成一部分工作后,在屏障点等待其他线程,全部线程到达后再一起继续执行后续任务。
- Semaphore:控制同时访问某个资源的线程个数。比如有一个资源限制最多只能有 5 个线程同时访问,就可以使用 Semaphore 来实现。
- 类加载过程:
- 加载:将类的字节码文件加载到内存中。
- 验证:检查加载的字节码文件是否符合 JVM 规范,确保其安全性。
- 准备:为类的静态变量分配内存,并设置默认初始值。
- 解析:将符号引用转换为直接引用。
- 初始化:执行类的静态代码块,为静态变量赋予正确的初始值。
- 方法区和堆内存区别:
- 方法区:主要存放类信息、常量、静态变量等数据。它是共享的,被所有类的实例所共用。方法区的内存回收相对较少。
- 堆内存:是对象实例的存储区域。对象在堆中创建、存储和销毁。堆内存的垃圾回收较为频繁。
- JVM 垃圾回收算法:
- 标记清除算法:先标记出所有需要回收的对象,然后统一回收这些对象所占用的内存空间。缺点是会产生大量不连续的内存碎片。
- 标记整理算法:标记出需要回收的对象后,将存活对象向一端移动,然后直接清理掉边界以外的内存。
- 复制算法:将内存分为两块,每次只使用其中一块,当这一块内存使用完后,将存活对象复制到另一块内存,然后清理原来的内存。适用于对象存活率较低的场景。
- 创建线程的方式:
- 继承 Thread 类:通过继承 Thread 类并重写 run 方法来定义线程的执行逻辑。
- 实现 Runnable 接口:实现 Runnable 接口的 run 方法,将 Runnable 对象作为参数传递给 Thread 类的构造函数来创建线程。
- 实现 Callable 接口:实现 Callable 接口的 call 方法,该方法有返回值。通过 FutureTask 来包装 Callable 对象,再将 FutureTask 对象作为参数传递给 Thread 类的构造函数创建线程。
- 线程池参数含义:
- corePoolSize:核心线程数,线程池初始化时创建的线程数。当提交的任务数小于 corePoolSize 时,会创建新线程来执行任务。
- maximumPoolSize:最大线程数,线程池能够容纳的最大线程数。当提交的任务数大于 corePoolSize 且任务队列已满时,会创建新线程直到线程数达到 maximumPoolSize。
- keepAliveTime:线程存活时间,当线程数大于 corePoolSize 时,多余的线程在空闲多长时间后会被销毁。
- unit:keepAliveTime 的时间单位。
- workQueue:任务队列,用于存放提交的任务,当线程数达到 corePoolSize 时,新提交的任务会被放入任务队列中。
- 任务数大于最大线程数时的处理:当提交的任务数大于线程池最大线程数时,任务会先进入任务队列(如果有空闲空间)。如果任务队列也已满,就会根据拒绝策略来处理新任务。拒绝策略有多种,比如 AbortPolicy(直接抛出异常)、DiscardPolicy(直接丢弃新任务)、DiscardOldestPolicy(丢弃队列中最旧的任务)、CallerRunsPolicy(由调用线程直接执行该任务) 。