并发常见问题与解决方案
概述
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() 操作不是原子的,可能发生指令重排:
- 分配内存空间
- 引用指向内存空间
- 初始化对象
如果步骤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:锁排序
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:
- 作用范围:volatile只能修饰变量,synchronized可以修饰方法、代码块
- 原子性:volatile不保证原子性,synchronized保证原子性
- 可见性:两者都保证可见性
- 有序性:volatile禁止指令重排,synchronized保证有序性
- 性能:volatile性能更高,synchronized有重量级锁开销
- 阻塞:volatile不会导致线程阻塞,synchronized会导致线程阻塞
Q: synchronized锁升级过程?
A:
- 偏向锁:只有一个线程访问时,对象头标记为偏向该线程
- 轻量级锁:有竞争时,升级为轻量级锁,使用CAS操作
- 重量级锁:竞争激烈时,升级为重量级锁,使用操作系统互斥量
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 代码规范
- 优先使用并发工具类:ConcurrentHashMap、CopyOnWriteArrayList等
- 减少锁的范围:只同步必要的代码块
- 避免嵌套锁:防止死锁
- 使用try-with-resources:确保锁的正确释放
- 处理中断异常:正确设置中断状态
10.2 性能优化
- 读写分离:读多写少场景使用ReadWriteLock
- 无锁编程:尽量使用CAS操作
- 线程池合理配置:根据任务类型配置参数
- 减少上下文切换:避免过度线程创建
10.3 调试技巧
- 使用JConsole:监控线程状态
- 使用Thread Dump:分析死锁和竞态条件
- 使用VisualVM:性能分析
- 日志记录:记录关键并发操作
通过以上内容,你应该能够应对大部分Java并发相关的面试问题。记住,理论理解很重要,但实际编码经验同样关键。