作为一名拥有七年 Java 开发经验的工程师,我深知并发编程是一把双刃剑 —— 用得好可以大幅提升系统性能,用不好则会引入各种难以调试的问题。本文将结合实际项目经验,深入分析并发编程中最常见的几类问题,并给出切实可行的解决方案。
一、死锁(Deadlock):系统的隐形杀手
1. 典型场景与问题表现
死锁是并发编程中最经典的问题之一,当两个或多个线程互相持有对方所需的锁,并且都在等待对方释放锁时,就会发生死锁。以下是一个典型的死锁场景:
public class DeadlockDemo {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
// 线程1:先获取lock1,再获取lock2
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: Holding lock 1 & 2...");
}
}
});
// 线程2:先获取lock2,再获取lock1
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: Holding lock 2 & 1...");
}
}
});
t1.start();
t2.start();
}
}
在这个例子中,线程 1 持有 lock1 并尝试获取 lock2,而线程 2 持有 lock2 并尝试获取 lock1,双方都在等待对方释放锁,从而导致死锁。
2. 解决方案
(1)保持锁的获取顺序一致
确保所有线程都按照相同的顺序获取锁,是避免死锁最有效的方法。例如,我们可以统一规定先获取 lock1 再获取 lock2:
public class DeadlockFreeSolution {
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...");
synchronized (lock2) {
System.out.println("Thread 1: Holding lock 1 & 2...");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 2: Holding lock 1...");
synchronized (lock2) {
System.out.println("Thread 2: Holding lock 1 & 2...");
}
}
});
t1.start();
t2.start();
}
}
(2)使用带超时的锁获取方法
使用ReentrantLock.tryLock(timeout)替代synchronized,在获取锁超时后释放已持有的锁:
import java.util.concurrent.locks.ReentrantLock;
public class TimeoutLockSolution {
private static final ReentrantLock lock1 = new ReentrantLock();
private static final ReentrantLock lock2 = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
if (lock1.tryLock(1, java.util.concurrent.TimeUnit.SECONDS)) {
try {
System.out.println("Thread 1: Holding lock 1...");
if (lock2.tryLock(1, java.util.concurrent.TimeUnit.SECONDS)) {
try {
System.out.println("Thread 1: Holding lock 1 & 2...");
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
t1.start();
}
}
二、活锁(Livelock):线程的 "忙等待" 陷阱
1. 问题表现与场景分析
活锁是另一种并发问题,线程虽然没有被阻塞,但由于不断重试相同的操作而无法继续执行。典型场景是两个线程互相响应对方的操作,导致无限循环。
以下是一个简单的活锁示例:
public class LivelockDemo {
private static class Resource {
private Resource owner;
public Resource getOwner() {
return owner;
}
public void setOwner(Resource owner) {
this.owner = owner;
}
public synchronized boolean tryTransfer(Resource target) {
if (owner == null) {
owner = target;
System.out.println(Thread.currentThread().getName() + " transferred resource to " + target);
return true;
}
return false;
}
}
public static void main(String[] args) {
Resource r1 = new Resource();
Resource r2 = new Resource();
Thread t1 = new Thread(() -> {
while (true) {
if (r1.tryTransfer(r2)) break;
try { Thread.sleep(100); } catch (InterruptedException e) {}
}
}, "Thread-1");
Thread t2 = new Thread(() -> {
while (true) {
if (r2.tryTransfer(r1)) break;
try { Thread.sleep(100); } catch (InterruptedException e) {}
}
}, "Thread-2");
t1.start();
t2.start();
}
}
在这个例子中,两个线程不断尝试将资源转移给对方,导致无限循环。
2. 解决方案
(1)引入随机延迟
在重试操作前引入随机延迟,避免线程间的同步重试:
Thread t1 = new Thread(() -> {
Random random = new Random();
while (true) {
if (r1.tryTransfer(r2)) break;
try {
// 引入随机延迟
Thread.sleep(random.nextInt(1000));
} catch (InterruptedException e) {}
}
});
(2)设置最大重试次数
限制每个线程的重试次数,超过阈值后进行降级处理:
java
int maxRetries = 100;
for (int i = 0; i < maxRetries; i++) {
if (r1.tryTransfer(r2)) break;
// 重试逻辑
}
// 超过最大重试次数,进行降级处理
三、线程安全集合的误用
1. 常见问题与风险
在使用线程安全集合时,开发人员常犯以下错误:
(1)认为所有操作都是原子的
虽然ConcurrentHashMap等集合是线程安全的,但复合操作(如get-then-put)仍需要额外的同步措施:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 非原子操作,可能导致竞态条件
if (!map.containsKey("key")) {
map.put("key", 1);
}
(2)迭代器的弱一致性问题
线程安全集合的迭代器是弱一致性的,可能无法反映最新的修改:
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("element1");
// 迭代器创建后,对list的修改不会反映在迭代器中
Iterator<String> it = list.iterator();
list.add("element2");
while (it.hasNext()) {
System.out.println(it.next()); // 只会输出element1
}
2. 解决方案
(1)使用原子操作替代复合操作
利用ConcurrentHashMap提供的原子方法:
// 使用putIfAbsent原子方法
map.putIfAbsent("key", 1);
// 使用computeIfAbsent处理复杂逻辑
map.computeIfAbsent("key", k -> expensiveCalculation(k));
(2)正确处理迭代器的弱一致性
在需要强一致性的场景下,使用显式锁:
ReentrantLock lock = new ReentrantLock();
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
public void iterateWithLock() {
lock.lock();
try {
// 在锁的保护下进行迭代
Iterator<String> it = list.iterator();
while (it.hasNext()) {
// 处理元素
}
} finally {
lock.unlock();
}
}
四、原子类的 ABA 问题
1. 问题原理与表现
原子类基于 CAS 操作,但存在 ABA 问题:当一个值从 A 变为 B 再变回 A 时,CAS 操作会认为值没有变化,但实际上已经发生了改变。
以下是一个 ABA 问题的示例:
import java.util.concurrent.atomic.AtomicInteger;
public class ABADemo {
private static AtomicInteger atomicInt = new AtomicInteger(100);
public static void main(String[] args) {
Thread mainThread = new Thread(() -> {
int expectedValue = atomicInt.get();
System.out.println("Main thread read value: " + expectedValue);
// 模拟其他线程修改
Thread otherThread = new Thread(() -> {
atomicInt.compareAndSet(100, 101);
System.out.println("Other thread changed value from 100 to 101");
atomicInt.compareAndSet(101, 100);
System.out.println("Other thread changed value from 101 back to 100");
});
otherThread.start();
otherThread.join();
// 主线程尝试CAS操作
boolean success = atomicInt.compareAndSet(expectedValue, 200);
System.out.println("Main thread CAS operation: " + success);
});
mainThread.start();
}
}
2. 解决方案
(1)使用带版本号的原子引用
AtomicStampedReference可以为值添加版本号,每次修改时版本号递增:
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABASolution {
private static AtomicStampedReference<Integer> atomicRef =
new AtomicStampedReference<>(100, 0);
public static void main(String[] args) {
int[] stampHolder = new int[1];
int expectedValue = atomicRef.get(stampHolder);
int expectedStamp = stampHolder[0];
// 模拟其他线程修改
Thread otherThread = new Thread(() -> {
int[] stampHolder1 = new int[1];
int value1 = atomicRef.get(stampHolder1);
int stamp1 = stampHolder1[0];
atomicRef.compareAndSet(value1, 101, stamp1, stamp1 + 1);
int[] stampHolder2 = new int[1];
int value2 = atomicRef.get(stampHolder2);
int stamp2 = stampHolder2[0];
atomicRef.compareAndSet(value2, 100, stamp2, stamp2 + 1);
});
otherThread.start();
otherThread.join();
// 主线程尝试CAS操作,同时检查值和版本号
boolean success = atomicRef.compareAndSet(expectedValue, 200, expectedStamp, expectedStamp + 1);
System.out.println("Main thread CAS operation: " + success);
}
}
五、锁的性能问题
1. 常见性能瓶颈
在高并发场景下,锁的不当使用会导致严重的性能问题:
(1)锁粒度太粗
使用类级锁或全局锁,导致大量线程阻塞:
public class CoarseGrainedLock {
private final Object lock = new Object();
private Map<String, Integer> data = new HashMap<>();
public void update(String key, int value) {
synchronized (lock) {
// 整个方法都被锁住,即使只有一小部分需要同步
data.put(key, value);
}
}
}
(2)锁持有时间过长
在锁的保护范围内执行耗时操作,如 IO、远程调用等:
public void process() {
synchronized (this) {
// 长时间的IO操作,其他线程需等待
performIOOperation();
}
}
2. 优化方案
(1)减小锁粒度
将大锁拆分为多个小锁,提高并发度:
public class FineGrainedLock {
private final Map<String, Integer> data = new ConcurrentHashMap<>();
private final Map<String, Object> locks = new ConcurrentHashMap<>();
public void update(String key, int value) {
// 为每个key创建单独的锁
Object keyLock = locks.computeIfAbsent(key, k -> new Object());
synchronized (keyLock) {
data.put(key, value);
}
}
}
(2)减少锁持有时间
将不需要同步的操作移出锁的范围:
public void process() {
// 非关键操作,无需同步
prepareData();
synchronized (this) {
// 只锁住关键操作
updateCriticalSection();
}
// 非关键操作,无需同步
postProcess();
}
六、并发编程最佳实践总结
根据多年的实战经验,我总结了以下并发编程最佳实践:
- 优先使用 JUC 包中的工具类:避免手动实现锁机制,优先使用ConcurrentHashMap、CopyOnWriteArrayList等成熟的并发集合。
- 使用原子类替代简单锁:对于计数、累加等简单操作,优先使用AtomicInteger等原子类,避免使用重量级锁。
- 保持锁的获取顺序一致:通过统一锁的获取顺序,避免死锁的发生。
- 减小锁粒度:避免使用大范围的锁,将锁的范围限制到最小必要部分。
- 警惕 ABA 问题:在使用原子类时,注意检查是否存在 ABA 问题,必要时使用AtomicStampedReference。
- 进行性能测试:在高并发场景下,不同锁实现的性能差异可能很大,建议进行压测后再做选择。
- 使用线程池管理线程:避免手动创建和销毁线程,使用ExecutorService创建线程池,提高线程复用率。
- 使用工具监控并发问题:利用 JVM 工具(如 jstack、jconsole)和第三方工具(如 VisualVM)监控和诊断并发问题。