资深Java工程师的面试题目(一)并发编程
以下是几道针对Java并发编程的面试题,涵盖基础知识、高级概念和实际应用场景,适合资深Java工程师的面试评估:
1. 线程池与任务调度
题目:
- 描述Java线程池的核心参数(如
corePoolSize
、maximumPoolSize
、keepAliveTime
等)的作用,并说明如何根据业务场景选择合适的线程池类型(如newFixedThreadPool
、newCachedThreadPool
)。 - 请解释以下代码片段中线程池的潜在问题,并提出改进建议:
ExecutorService executor = Executors.newFixedThreadPool(10); for (int i = 0; i < 1000; i++) {executor.submit(() -> {// 执行耗时操作}); }
参考答案:
-
核心参数作用:
corePoolSize
:核心线程数,线程池中始终存活的线程数量。maximumPoolSize
:最大线程数,线程池允许创建的最大线程数。keepAliveTime
:非核心线程的存活时间,超过核心数的线程在空闲后会被销毁。workQueue
:任务队列,用于缓存未执行的任务。threadFactory
:线程工厂,用于创建线程。handler
:拒绝策略,当任务队列和线程数均满时的处理方式。
-
线程池类型选择:
newFixedThreadPool
:固定大小的线程池,适合负载稳定的场景(如Web服务器)。newCachedThreadPool
:可缓存的线程池,适合短时任务(如异步处理)。newScheduledThreadPool
:支持定时任务,适合周期性任务(如定时清理缓存)。
-
代码问题:
- 使用
newFixedThreadPool(10)
提交1000个任务,可能导致任务队列堆积(默认使用无界队列LinkedBlockingQueue
),占用大量内存甚至导致OOM。 - 改进建议:
- 指定有界队列(如
ArrayBlockingQueue
),并设置合理的拒绝策略(如CallerRunsPolicy
)。 - 根据任务类型调整线程池大小(如CPU密集型任务线程数为
CPU核心数 + 1
,IO密集型任务线程数可更高)。
- 指定有界队列(如
- 使用
2. 锁与同步机制
题目:
- 比较
synchronized
关键字和ReentrantLock
的优缺点,并说明在哪些场景下更适合使用ReentrantLock
。 - 请分析以下代码中的线程安全问题,并提供解决方案:
public class Counter {private int count = 0;public void increment() {count++;} }
参考答案:
-
synchronized
vsReentrantLock
:- 相同点:都支持可重入性,能解决线程安全问题。
- 不同点:
- 灵活性:
ReentrantLock
支持尝试加锁(tryLock
)、超时加锁、条件变量(Condition
)等高级功能,而synchronized
只能通过隐式锁实现。 - 性能:在低竞争场景下,
synchronized
性能接近ReentrantLock
;在高竞争场景下,ReentrantLock
可通过公平锁策略减少线程饥饿。 - 使用方式:
synchronized
通过语法糖实现(如方法或代码块),而ReentrantLock
需要显式加锁和释放(需在finally
中释放)。
- 灵活性:
-
代码问题:
count++
操作(即count = count + 1
)不是原子操作,存在线程安全问题(多个线程可能同时读取并更新count
的旧值)。- 解决方案:
- 使用
synchronized
修饰increment
方法。 - 使用
ReentrantLock
手动加锁。 - 使用原子类
AtomicInteger
替代普通int
。
- 使用
3. 并发工具类
题目:
- 请说明
CountDownLatch
、CyclicBarrier
和Semaphore
的使用场景及区别,并提供一个实际应用案例。 - 编写一个使用
CyclicBarrier
的示例代码,模拟多个线程协作完成任务的场景。
参考答案:
-
工具类对比:
CountDownLatch
:用于等待一组线程完成任务后继续执行(如主线程等待所有子线程完成)。CyclicBarrier
:用于多个线程相互等待,达到屏障点后再同时继续执行(如多线程并行计算后汇总结果)。Semaphore
:用于控制同时访问某个资源的线程数量(如限流)。
-
案例:
CyclicBarrier
示例:模拟3个线程分别计算数组的不同部分,汇总结果后再输出:public class CyclicBarrierExample {public static void main(String[] args) {CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("All threads have finished their part!"));int[] data = {1, 2, 3, 4, 5, 6};for (int i = 0; i < 3; i++) {new Thread(() -> {int sum = 0;for (int j = 0; j < 2; j++) {sum += data[ThreadLocalRandom.current().nextInt(data.length)];}System.out.println("Thread " + Thread.currentThread().getId() + " computed sum: " + sum);try {barrier.await();} catch (Exception e) {e.printStackTrace();}}).start();}} }
4. 性能优化与死锁
题目:
- 如何通过JVM参数和工具(如
jstack
、VisualVM
)定位和解决线程死锁问题? - 请分析以下代码可能引发死锁的原因,并提出优化方案:
public class Deadlock {private final Object lock1 = new Object();private final Object lock2 = new Object();public void methodA() {synchronized (lock1) {synchronized (lock2) {// 业务逻辑}}}public void methodB() {synchronized (lock2) {synchronized (lock1) {// 业务逻辑}}} }
参考答案:
-
死锁定位与解决:
- 使用
jstack <pid>
生成线程堆栈,查找BLOCKED
状态的线程及锁依赖关系。 - 使用
VisualVM
的线程分析工具查看线程状态和锁竞争情况。 - 解决方案:避免嵌套锁,或统一锁顺序(如始终先获取
lock1
再获取lock2
)。
- 使用
-
代码问题:
- 死锁原因:
methodA
和methodB
以相反顺序获取锁(lock1
→lock2
vslock2
→lock1
),可能导致两个线程互相等待对方释放锁。 - 优化方案:
- 统一锁顺序(如始终先获取
lock1
再获取lock2
)。 - 使用
ReentrantLock.tryLock()
尝试加锁,超时后回退。
- 统一锁顺序(如始终先获取
- 死锁原因:
5. 高级并发模式
题目:
- 请描述生产者-消费者模型的实现方式,并说明如何通过
BlockingQueue
优化该模型。 - 使用
CompletableFuture
编写一个异步任务链,要求:- 并行执行两个任务(如查询数据库和查询缓存)。
- 合并结果并返回最终数据。
参考答案:
-
生产者-消费者模型:
- 使用
BlockingQueue
(如ArrayBlockingQueue
)实现线程间通信,生产者将任务放入队列,消费者从队列取出任务处理。 - 优点:解耦生产者与消费者,避免忙等(busy-wait)。
- 使用
-
CompletableFuture
示例:public class AsyncExample {public static void main(String[] args) {CompletableFuture<String> dbFuture = CompletableFuture.supplyAsync(() -> {// 模拟数据库查询return "DB Result";});CompletableFuture<String> cacheFuture = CompletableFuture.supplyAsync(() -> {// 模拟缓存查询return "Cache Result";});dbFuture.thenCombine(cacheFuture, (db, cache) -> {// 合并结果return db + " + " + cache;}).thenAccept(result -> {System.out.println("Final Result: " + result);});} }
6. JVM与并发安全
题目:
- 解释
volatile
关键字的作用原理,并说明它与synchronized
在可见性、原子性和有序性上的区别。 - 请分析以下代码为何不满足线程安全,并提出改进方案:
public class VolatileExample {private volatile int counter = 0;public void increment() {counter++;} }
参考答案:
-
volatile
原理:volatile
通过内存屏障(Memory Barrier)确保变量的可见性和禁止指令重排序,但不保证原子性。- 与
synchronized
的区别:- 可见性:两者均保证可见性。
- 原子性:
synchronized
保证原子性,volatile
不保证。 - 有序性:
volatile
通过禁止指令重排序保证部分有序性,而synchronized
通过锁的释放和获取保证整体有序性。
-
代码问题:
counter++
操作(read
→increment
→write
)不是原子的,即使counter
是volatile
,多个线程仍可能覆盖彼此的修改。- 改进方案:
- 使用
synchronized
修饰increment
方法。 - 使用原子类
AtomicInteger
。
- 使用
7. 实战问题
题目:
- 设计一个线程安全的缓存类,要求:
- 支持并发读取和写入。
- 提供过期时间(TTL)功能,自动清除过期数据。
- 支持高并发下的性能优化。
参考答案:
- 设计思路:
- 使用
ConcurrentHashMap
存储缓存数据,键为缓存项的唯一标识,值为带有TTL的封装对象。 - 使用
ReentrantReadWriteLock
实现读写分离:- 读操作共享锁,允许多线程并发读。
- 写操作独占锁,确保单线程写入。
- 使用定时任务(如
ScheduledExecutorService
)定期清理过期数据。 - 代码示例:
public class ConcurrentCache<K, V> {private final ConcurrentHashMap<K, CacheEntry<V>> cache = new ConcurrentHashMap<>();private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);public ConcurrentCache() {scheduler.scheduleAtFixedRate(this::cleanUp, 1, 1, TimeUnit.MINUTES);}public V get(K key) {lock.readLock().lock();try {CacheEntry<V> entry = cache.get(key);if (entry == null || entry.isExpired()) {return null;}return entry.getValue();} finally {lock.readLock().unlock();}}public void put(K key, V value, long ttl, TimeUnit unit) {lock.writeLock().lock();try {cache.put(key, new CacheEntry<>(value, System.currentTimeMillis() + unit.toMillis(ttl)));} finally {lock.writeLock().unlock();}}private void cleanUp() {lock.writeLock().lock();try {cache.entrySet().removeIf(entry -> entry.getValue().isExpired());} finally {lock.writeLock().unlock();}}private static class CacheEntry<V> {private final V value;private final long expireTime;public CacheEntry(V value, long expireTime) {this.value = value;this.expireTime = expireTime;}public V getValue() {return value;}public boolean isExpired() {return System.currentTimeMillis() > expireTime;}} }
- 使用
以上题目覆盖了Java并发编程的核心知识点,包括线程池、锁机制、并发工具类、性能优化、死锁处理以及高阶设计模式。通过这些问题,可以全面评估候选人对并发编程的理解和实际应用能力。