面试官:请简要介绍一下 Java 核心知识中面向对象的三大特性。
王铁牛:嗯,这个我知道,面向对象的三大特性是封装、继承和多态。封装就是把对象的属性和方法包装起来,对外提供统一的接口;继承是指一个类可以继承另一个类的属性和方法;多态就是同一个方法可以根据对象的不同类型而表现出不同的行为。
面试官:不错,回答得很准确。那在多线程环境下,如何保证数据的一致性?
王铁牛:可以使用 synchronized 关键字来同步代码块或者方法,也可以使用 Lock 接口及其实现类来实现锁机制。另外,还可以使用 volatile 关键字来保证变量的可见性。
面试官:很好。接下来问你几个关于 JUC 的问题。请解释一下 CountDownLatch 和 CyclicBarrier 的区别。
王铁牛:呃……这个嘛,CountDownLatch 是用来等待一组操作完成后再继续执行,它的计数器是递减的,当计数器为 0 时,所有等待的线程可以继续执行。而 CyclicBarrier 是用来让一组线程互相等待,直到所有线程都到达某个屏障点,然后再一起继续执行,它的计数器是可以循环使用的。
面试官:回答得不太清晰。再问你,如何创建一个固定大小的线程池?
王铁牛:可以使用 Executors 类的 newFixedThreadPool 方法来创建一个固定大小的线程池。
面试官:好,第一轮面试到此结束。
第二轮:
面试官:请说一下 JVM 的内存结构。
王铁牛:JVM 的内存结构主要包括堆、栈、方法区、程序计数器、本地方法栈。堆是用来存储对象实例的,栈是用来存储局部变量和方法调用的,方法区是用来存储类信息、常量、静态变量等,程序计数器是用来记录当前线程执行的字节码指令地址,本地方法栈是用来执行本地方法的。
面试官:那在 JVM 中,对象的创建过程是怎样的?
王铁牛:首先,JVM 会在堆中为对象分配内存空间,然后初始化对象的成员变量,接着调用对象的构造函数进行初始化,最后将对象的引用返回给调用者。
面试官:如果对象创建过程中发生了内存溢出,可能是什么原因?
王铁牛:可能是因为堆内存不够用了,也可能是因为对象创建的速度太快,导致内存分配来不及。
面试官:这一轮回答得还算可以。最后一个问题,如何进行 JVM 的性能调优?
王铁牛:可以通过调整堆内存大小、垃圾收集器参数、线程池大小等方式来进行 JVM 的性能调优。
面试官:第二轮面试结束。
第三轮:
面试官:请讲一下 HashMap 的底层实现原理。
王铁牛:HashMap 是基于数组和链表实现的,它通过 key 的哈希值来计算数组的下标,如果有多个 key 的哈希值相同,就会形成链表。在 JDK 1.8 之后,当链表长度超过一定阈值时,会将链表转换为红黑树,以提高查询效率。
面试官:那在多线程环境下,使用 HashMap 会有什么问题?
王铁牛:在多线程环境下,使用 HashMap 可能会导致数据丢失或者死循环,因为 HashMap 不是线程安全的。
面试官:如何解决这个问题?
王铁牛:可以使用 Collections.synchronizedMap 方法或者使用 ConcurrentHashMap 来替代 HashMap,ConcurrentHashMap 是线程安全的。
面试官:第三轮面试完毕。回家等通知吧。
答案:
- Java 核心知识中面向对象的三大特性:
- 封装:把对象的属性和方法包装起来,对外提供统一的接口。这样可以隐藏对象的内部实现细节,提高代码的安全性和可维护性。例如,一个类中的属性可以使用 private 修饰,然后通过 public 的方法来访问和修改这些属性。
- 继承:一个类可以继承另一个类的属性和方法。继承可以实现代码的复用,提高开发效率。比如,定义一个父类 Animal,子类 Dog 和 Cat 可以继承 Animal 的属性(如 name、age 等)和方法(如 eat 方法),然后根据自身特点进行扩展。
- 多态:同一个方法可以根据对象的不同类型而表现出不同的行为。多态的实现方式有两种:编译时多态(方法重载)和运行时多态(方法重写)。例如,定义一个父类 Shape,子类 Circle 和 Rectangle 重写父类的 draw 方法,当调用 draw 方法时,根据对象的实际类型调用相应子类的 draw 方法。
- 在多线程环境下保证数据一致性的方法:
- synchronized 关键字:
- 可以修饰代码块或者方法。当一个线程访问被 synchronized 修饰的代码块或方法时,会先获取对象的锁。如果锁被其他线程占用,该线程会进入等待状态,直到锁被释放。例如:
- synchronized 关键字:
public class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++;
}
}
- 修饰代码块时,格式为`synchronized(对象) { // 代码 }`,这里的对象可以是任何对象,通常是 this 或者一个共享的对象。
- Lock 接口及其实现类:
- Lock 接口提供了比 synchronized 更灵活的锁控制。例如 ReentrantLock 类,它可以实现公平锁和可中断锁。
- 示例代码:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private int count = 0;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
- volatile 关键字:保证变量的可见性。当一个变量被声明为 volatile 时,它会保证对该变量的写操作会立即刷新到主内存中,读操作会从主内存中读取最新的值,而不是从线程的工作内存中读取。例如:
public class VolatileExample {
private volatile int value;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
- CountDownLatch 和 CyclicBarrier 的区别:
- CountDownLatch:
- 用来等待一组操作完成后再继续执行。它的计数器是递减的,当计数器为 0 时,所有等待的线程可以继续执行。
- 例如,有一个任务需要等待多个子任务完成后再执行,可以使用 CountDownLatch。主线程调用 CountDownLatch 的 await 方法等待,子任务完成后调用 CountDownLatch 的 countDown 方法,当计数器减为 0 时,主线程继续执行。
- CyclicBarrier:
- 用来让一组线程互相等待,直到所有线程都到达某个屏障点,然后再一起继续执行。它的计数器是可以循环使用的。
- 比如,多个运动员进行接力比赛,每个运动员到达一个屏障点后等待其他运动员,直到所有运动员都到达屏障点,然后一起继续比赛。可以使用 CyclicBarrier 来实现这种同步机制。
- CountDownLatch:
- 创建固定大小的线程池: 可以使用 Executors 类的 newFixedThreadPool 方法。示例代码如下:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThreadPoolExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
executorService.submit(() -> {
System.out.println(Thread.currentThread().getName() + " is running");
});
}
executorService.shutdown();
}
}
这里创建了一个固定大小为 5 的线程池,提交了 10 个任务,线程池会按照固定大小的线程数量来执行这些任务。 5. JVM 的内存结构:
- 堆:用来存储对象实例。是 JVM 内存中最大的一块区域,也是垃圾回收的主要区域。
- 栈:用来存储局部变量和方法调用。每个线程都有自己独立的栈空间,栈中的数据随着方法的调用和返回而变化。
- 方法区:用来存储类信息、常量、静态变量等。在 JDK 1.8 之后,方法区被元空间(MetaSpace)取代,元空间使用本地内存,而不是 JVM 堆内存。
- 程序计数器:用来记录当前线程执行的字节码指令地址。是线程私有的,每个线程都有自己的程序计数器。
- 本地方法栈:用来执行本地方法(用 C、C++ 等编写的方法)。同样是线程私有的。
- 对象的创建过程:
- 首先,JVM 会在堆中为对象分配内存空间。这一步会检查堆内存是否足够,如果不够可能会触发垃圾回收或者抛出 OutOfMemoryError 异常。
- 然后,初始化对象的成员变量。将成员变量初始化为默认值,比如 int 类型初始化为 0,引用类型初始化为 null 等。
- 接着,调用对象的构造函数进行初始化。构造函数会按照代码中的逻辑对对象进行进一步的初始化操作。
- 最后,将对象的引用返回给调用者。调用者可以通过这个引用访问和操作对象。
- 对象创建过程中发生内存溢出的原因:
- 堆内存不够用:当创建的对象数量过多,或者对象占用的内存过大,导致堆内存无法容纳新创建的对象时,就会发生内存溢出。例如,不断创建大对象,或者在一个循环中创建大量对象且这些对象长时间不被回收。
- 对象创建速度太快:如果在短时间内创建大量对象,而垃圾回收机制来不及回收这些对象,也可能导致内存分配来不及,从而引发内存溢出。比如在高并发场景下,大量线程同时创建对象。
- 进行 JVM 性能调优的方法:
- 调整堆内存大小:可以通过修改 JVM 的启动参数来调整堆内存大小。例如,使用
-Xmx和-Xms参数,-Xmx表示最大堆内存,-Xms表示初始堆内存。如果应用程序在运行过程中需要较大的堆内存,可以适当增大-Xmx的值;如果希望 JVM 启动时不要占用过多内存,可以调整-Xms的值,使其与-Xmx的值相等或者接近。 - 垃圾收集器参数调整:不同的垃圾收集器适用于不同的应用场景。可以根据应用的特点选择合适的垃圾收集器,并调整其相关参数。例如,对于新生代比较大且对象创建和销毁频率高的应用,可以选择 Parallel Scavenge 收集器,并通过
-XX:NewRatio参数调整新生代和老年代的比例;对于注重低延迟的应用,可以考虑使用 CMS 收集器,并调整其相关参数如-XX:CMSInitiatingOccupancyFraction来控制何时启动 CMS 垃圾回收。 - 线程池大小调整:合理设置线程池的大小可以提高应用的性能。如果线程池过大,会浪费系统资源;如果线程池过小,可能导致任务排队等待,影响响应速度。可以根据应用的并发量、任务执行时间等因素来调整线程池的大小。例如,使用
ThreadPoolExecutor类来创建线程池时,可以通过调整其核心线程数、最大线程数、队列容量等参数来优化线程池性能。
- 调整堆内存大小:可以通过修改 JVM 的启动参数来调整堆内存大小。例如,使用
- HashMap 的底层实现原理:
- HashMap 是基于数组和链表实现的。它通过 key 的哈希值来计算数组的下标。
- 当向 HashMap 中插入键值对时,首先计算 key 的哈希值,然后通过哈希值与数组长度进行位运算得到数组下标。
- 如果该下标位置为空,直接将键值对插入该位置。
- 如果该下标位置不为空,说明发生了哈希冲突。此时会遍历链表(JDK 1.8 之前)或者链表和红黑树(JDK 1.8 之后),如果找到相同的 key,则更新其 value;如果没有找到相同的 key,则将新的键值对插入到链表或红黑树的末尾。
- 在 JDK 1.8 之后,当链表长度超过一定阈值(默认是 8)时,链表会转换为红黑树,以提高查询效率。当红黑树节点数量小于等于 6 时,又会转换回链表。
- 在多线程环境下使用 HashMap 的问题及解决方法:
- 问题:
- 在多线程环境下,使用 HashMap 可能会导致数据丢失或者死循环。因为 HashMap 在扩容时会进行 rehash 操作,这个过程可能会导致链表形成环形结构,从而在后续的查询操作中出现死循环。同时,在多线程同时进行 put 操作时,可能会导致数据覆盖等问题。
- 解决方法:
- 使用 Collections.synchronizedMap 方法:
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class SynchronizedMapExample {
public static void main(String[] args) {
Map<String, Integer> map = Collections.synchronizedMap(new HashMap<>());
map.put("key1", 1);
map.get("key1");
}
}
- **使用 ConcurrentHashMap**:ConcurrentHashMap 是线程安全的哈希表。它采用了分段锁机制,在进行写操作时只对部分段加锁,从而提高了并发性能。例如:
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key1", 1);
map.get("key1");
}
}