跳到主要内容

并发常见问题与解决方案

概述

Java并发编程是后端开发中的核心技能,也是面试中的高频考点。本文档总结了常见的并发问题、场景分析及解决方案。

一、线程安全问题

1.1 原子性问题

场景描述:

// 线程不安全的计数器
public class UnsafeCounter {
private int count = 0;

public void increment() {
count++; // 非原子操作:读取->修改->写入
}

public int getCount() {
return count;
}
}

问题分析: count++ 操作包含三个步骤:读取count值、加1、写回新值。多线程环境下可能出现数据竞争。

解决方案:

方案1:使用Atomic原子类

public class SafeCounter {
private final AtomicInteger count = new AtomicInteger(0);

public void increment() {
count.incrementAndGet(); // 原子操作
}

public int getCount() {
return count.get();
}
}

方案2:使用synchronized关键字

public class SafeCounter {
private int count = 0;

public synchronized void increment() {
count++;
}

public synchronized int getCount() {
return count;
}
}

方案3:使用ReentrantLock

public class SafeCounter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();

public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}

public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}

1.2 复合操作的原子性

场景描述:

// 检查再操作的经典问题
public class ListHelper {
public List<String> list = new ArrayList<>();

public void addIfAbsent(String item) {
if (!list.contains(item)) { // 检查
list.add(item); // 操作
}
}
}

问题分析: 在检查和操作之间,其他线程可能修改了list的状态,导致逻辑错误。

解决方案:

方案1:同步整个方法

public synchronized void addIfAbsent(String item) {
if (!list.contains(item)) {
list.add(item);
}
}

方案2:使用并发集合

public class ListHelper {
private final ConcurrentHashMap<String, Boolean> map = new ConcurrentHashMap<>();

public void addIfAbsent(String item) {
map.putIfAbsent(item, Boolean.TRUE);
}
}

二、可见性问题

2.1 指令重排导致的问题

场景描述:

public class DoubleCheckedLocking {
private static volatile Resource resource; // 必须使用volatile

public static Resource getInstance() {
if (resource == null) { // 第一次检查
synchronized (DoubleCheckedLocking.class) {
if (resource == null) { // 第二次检查
resource = new Resource(); // 可能指令重排
}
}
}
return resource;
}
}

class Resource {
// 大量字段初始化
}

问题分析: new Resource() 操作不是原子的,可能发生指令重排:

  1. 分配内存空间
  2. 引用指向内存空间
  3. 初始化对象

如果步骤2和3重排,其他线程可能看到未完全初始化的对象。

解决方案:

// 方案1:使用volatile禁止指令重排
private static volatile Resource resource;

// 方案2:使用静态内部类(推荐)
public class DoubleCheckedLocking {
private static class Holder {
static final Resource INSTANCE = new Resource();
}

public static Resource getInstance() {
return Holder.INSTANCE;
}
}

// 方案3:使用枚举单例
public enum ResourceEnum {
INSTANCE;

public void doSomething() {
// 业务逻辑
}
}

2.2 缓存一致性问题

场景描述:

public class VisibilityDemo {
private boolean running = true;

public void start() {
new Thread(() -> {
while (running) { // 可能永远看不到running=false
// 工作
}
System.out.println("Thread stopped");
}).start();
}

public void stop() {
running = false;
}
}

解决方案:

public class VisibilityDemo {
private volatile boolean running = true; // 使用volatile

// 或者使用AtomicBoolean
private final AtomicBoolean running = new AtomicBoolean(true);
}

三、有序性问题

3.1 Happens-Before原则

场景描述:

public class OrderIssue {
private int value = 0;
private boolean flag = false;

// 线程A执行
public void writer() {
value = 1; // 操作1
flag = true; // 操作2
}

// 线程B执行
public void reader() {
if (flag) { // 操作3
int r = value; // 操作4
System.out.println(r);
}
}
}

问题分析: 如果没有happens-before关系,操作3可能看到flag=true,但操作4可能看到value=0。

解决方案:

public class OrderIssue {
private int value = 0;
private volatile boolean flag = false; // 使用volatile

public void writer() {
value = 1; // volatile写之前的操作
flag = true; // volatile写
}

public void reader() {
if (flag) { // volatile读
int r = value; // volatile读之后的操作能看到之前的写
System.out.println(r);
}
}
}

四、死锁问题

4.1 经典死锁场景

场景描述:

public class DeadlockDemo {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();

public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1: Holding lock 1...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread 1: Waiting for lock 2...");
synchronized (lock2) {
System.out.println("Thread 1: Acquired lock 2!");
}
}
});

Thread t2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2: Holding lock 2...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread 2: Waiting for lock 1...");
synchronized (lock1) {
System.out.println("Thread 2: Acquired lock 1!");
}
}
});

t1.start();
t2.start();
}
}

死锁的四个必要条件:

  1. 互斥条件:资源不能共享
  2. 请求与保持:持有资源的同时请求其他资源
  3. 不剥夺条件:不能强制释放已持有的资源
  4. 循环等待:存在等待环路

解决方案:

方案1:锁排序

public class DeadlockSolution {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();

// 始终按照固定顺序获取锁
public static void method1() {
synchronized (lock1) {
synchronized (lock2) {
// 业务逻辑
}
}
}

public static void method2() {
synchronized (lock1) { // 同样先获取lock1
synchronized (lock2) {
// 业务逻辑
}
}
}
}

方案2:使用tryLock超时

public class TryLockSolution {
private final ReentrantLock lock1 = new ReentrantLock();
private final ReentrantLock lock2 = new ReentrantLock();

public void transfer() throws InterruptedException {
while (true) {
if (lock1.tryLock()) {
try {
if (lock2.tryLock()) {
try {
// 业务逻辑
break;
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
Thread.sleep(10); // 避免忙等待
}
}
}

方案3:使用一个账户锁

public class AccountLockSolution {
private final Object accountLock = new Object();

public void transfer(Account from, Account to, double amount) {
synchronized (accountLock) { // 使用全局锁
if (from.getBalance() >= amount) {
from.debit(amount);
to.credit(amount);
}
}
}
}

五、线程池相关面试问题

5.1 线程池参数配置

场景描述:

// 不合理的线程池配置
ThreadPoolExecutor executor = new ThreadPoolExecutor(
100, // corePoolSize过大
200, // maximumPoolSize过大
60L, // keepAliveTime
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000), // 无界队列可能导致OOM
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);

合理配置方案:

CPU密集型任务:

// 线程数 = CPU核心数 + 1
int corePoolSize = Runtime.getRuntime().availableProcessors() + 1;
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
corePoolSize,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(100)
);

IO密集型任务:

// 线程数 = CPU核心数 * 2
int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
corePoolSize * 2,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000)
);

5.2 线程池拒绝策略

内置拒绝策略使用场景:

// CallerRunsPolicy - 让提交任务的线程执行
new ThreadPoolExecutor.CallerRunsPolicy()

// AbortPolicy - 默认策略,抛出异常
new ThreadPoolExecutor.AbortPolicy()

// DiscardPolicy - 静默丢弃任务
new ThreadPoolExecutor.DiscardPolicy()

// DiscardOldestPolicy - 丢弃队列中最老的任务
new ThreadPoolExecutor.DiscardOldestPolicy()

自定义拒绝策略:

public class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 记录日志
logger.warn("Task rejected: {}", r.toString());

// 尝试放入备用队列
if (!backupQueue.offer(r)) {
// 降级处理
fallbackHandler(r);
}
}
}

六、并发集合相关问题

6.1 ConcurrentHashMap分段锁

场景描述:

// Map的线程安全使用
public class ConcurrentMapUsage {
private final ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

// 原子操作
public void putIfAbsent(String key, int value) {
map.putIfAbsent(key, value);
}

// 复合操作需要额外同步
public void update(String key, int delta) {
// 方案1:使用compute
map.compute(key, (k, v) -> (v == null) ? delta : v + delta);

// 方案2:使用merge(更简洁)
map.merge(key, delta, Integer::sum);
}

// 迭代操作
public void processAll() {
map.forEach((key, value) -> {
// 处理每个键值对
processKeyValue(key, value);
});
}
}

6.2 CopyOnWriteArrayList使用场景

适用场景:

public class EventListeners {
private final CopyOnWriteArrayList<EventListener> listeners =
new CopyOnWriteArrayList<>();

public void addListener(EventListener listener) {
listeners.add(listener);
}

public void removeListener(EventListener listener) {
listeners.remove(listener);
}

// 读多写少的场景
public void fireEvent(Event event) {
for (EventListener listener : listeners) {
listener.onEvent(event);
}
}
}

注意事项:

  • 适用于读操作远多于写操作的场景
  • 写操作时会复制整个数组,内存消耗较大
  • 迭代器不会抛出ConcurrentModificationException

七、生产者消费者模式

7.1 使用BlockingQueue实现

public class ProducerConsumerPattern {
private final BlockingQueue<Task> queue = new ArrayBlockingQueue<>(100);

// 生产者
class Producer implements Runnable {
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
Task task = produceTask();
queue.put(task); // 队列满时阻塞
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

// 消费者
class Consumer implements Runnable {
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
Task task = queue.take(); // 队列空时阻塞
processTask(task);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}

7.2 使用Condition实现

public class ProducerConsumerWithCondition {
private final Queue<Task> queue = new LinkedList<>();
private final int capacity = 100;
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();

public void produce(Task task) throws InterruptedException {
lock.lock();
try {
while (queue.size() >= capacity) {
notFull.await();
}
queue.offer(task);
notEmpty.signal();
} finally {
lock.unlock();
}
}

public Task consume() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await();
}
Task task = queue.poll();
notFull.signal();
return task;
} finally {
lock.unlock();
}
}
}

八、面试常问的并发工具类

8.1 CountDownLatch

应用场景:

public class CountDownLatchDemo {
public void processWithWorkers() throws InterruptedException {
int workerCount = 5;
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(workerCount);

// 创建并启动工作线程
for (int i = 0; i < workerCount; i++) {
new Thread(new Worker(startSignal, doneSignal, i)).start();
}

// 所有线程准备好后,同时开始
System.out.println("Ready to start...");
startSignal.countDown();

// 等待所有工作线程完成
doneSignal.await();
System.out.println("All workers completed");
}

static class Worker implements Runnable {
private final CountDownLatch startSignal;
private final CountDownLatch doneSignal;
private final int id;

Worker(CountDownLatch startSignal, CountDownLatch doneSignal, int id) {
this.startSignal = startSignal;
this.doneSignal = doneSignal;
this.id = id;
}

@Override
public void run() {
try {
startSignal.await(); // 等待开始信号
doWork();
doneSignal.countDown(); // 完成工作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

private void doWork() {
System.out.println("Worker " + id + " is working");
// 模拟工作
}
}
}

8.2 CyclicBarrier

应用场景:

public class CyclicBarrierDemo {
private final CyclicBarrier barrier;

public CyclicBarrierDemo(int parties) {
this.barrier = new CyclicBarrier(parties, () -> {
System.out.println("All parties arrived at barrier, proceeding...");
});
}

public void startSimulation() {
for (int i = 0; i < barrier.getParties(); i++) {
new Thread(new Worker(i)).start();
}
}

class Worker implements Runnable {
private final int id;

Worker(int id) {
this.id = id;
}

@Override
public void run() {
try {
// 第一阶段工作
doPhase1();
barrier.await(); // 等待其他线程

// 第二阶段工作
doPhase2();
barrier.await(); // 再次等待

// 第三阶段工作
doPhase3();
} catch (Exception e) {
Thread.currentThread().interrupt();
}
}

private void doPhase1() {
System.out.println("Worker " + id + " completed phase 1");
}

private void doPhase2() {
System.out.println("Worker " + id + " completed phase 2");
}

private void doPhase3() {
System.out.println("Worker " + id + " completed phase 3");
}
}
}

8.3 Semaphore

应用场景:

public class SemaphoreDemo {
private final Semaphore semaphore;

public SemaphoreDemo(int permits) {
this.semaphore = new Semaphore(permits);
}

public void accessResource() {
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可
try {
// 访问受限资源
accessLimitedResource();
} finally {
semaphore.release(); // 释放许可
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}

private void accessLimitedResource() {
System.out.println(Thread.currentThread().getName() +
" is accessing resource");
// 模拟资源访问
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

九、常见面试题总结

9.1 基础概念题

Q: volatile关键字和synchronized关键字的区别?

A:

  1. 作用范围:volatile只能修饰变量,synchronized可以修饰方法、代码块
  2. 原子性:volatile不保证原子性,synchronized保证原子性
  3. 可见性:两者都保证可见性
  4. 有序性:volatile禁止指令重排,synchronized保证有序性
  5. 性能:volatile性能更高,synchronized有重量级锁开销
  6. 阻塞:volatile不会导致线程阻塞,synchronized会导致线程阻塞

Q: synchronized锁升级过程?

A:

  1. 偏向锁:只有一个线程访问时,对象头标记为偏向该线程
  2. 轻量级锁:有竞争时,升级为轻量级锁,使用CAS操作
  3. 重量级锁:竞争激烈时,升级为重量级锁,使用操作系统互斥量

9.2 实际应用题

Q: 如何设计一个线程安全的单例模式?

A:

// 推荐方案:枚举单例
public enum Singleton {
INSTANCE;

public void doSomething() {
// 业务逻辑
}
}

// 方案2:双重检查锁定
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;
}
}

Q: 如何实现一个高效的缓存?

A:

public class ConcurrentCache<K, V> {
private final ConcurrentHashMap<K, CompletableFuture<V>> cache =
new ConcurrentHashMap<>();

public V get(K key, Function<K, V> loader) {
return cache.computeIfAbsent(key, k ->
CompletableFuture.supplyAsync(() -> loader.apply(k))
).join();
}

// 防止缓存击穿
public V getWithLock(K key, Function<K, V> loader) {
while (true) {
CompletableFuture<V> future = cache.get(key);
if (future != null) {
return future.join();
}

CompletableFuture<V> newFuture = new CompletableFuture<>();
CompletableFuture<V> existing = cache.putIfAbsent(key, newFuture);
if (existing == null) {
try {
V value = loader.apply(key);
newFuture.complete(value);
return value;
} catch (Exception e) {
newFuture.completeExceptionally(e);
cache.remove(key);
throw new RuntimeException(e);
}
} else {
return existing.join();
}
}
}
}

十、最佳实践

10.1 代码规范

  1. 优先使用并发工具类:ConcurrentHashMap、CopyOnWriteArrayList等
  2. 减少锁的范围:只同步必要的代码块
  3. 避免嵌套锁:防止死锁
  4. 使用try-with-resources:确保锁的正确释放
  5. 处理中断异常:正确设置中断状态

10.2 性能优化

  1. 读写分离:读多写少场景使用ReadWriteLock
  2. 无锁编程:尽量使用CAS操作
  3. 线程池合理配置:根据任务类型配置参数
  4. 减少上下文切换:避免过度线程创建

10.3 调试技巧

  1. 使用JConsole:监控线程状态
  2. 使用Thread Dump:分析死锁和竞态条件
  3. 使用VisualVM:性能分析
  4. 日志记录:记录关键并发操作

通过以上内容,你应该能够应对大部分Java并发相关的面试问题。记住,理论理解很重要,但实际编码经验同样关键。