Java并发编程的常见问题总结

Java并发编程的常见问题总结

并发编程是 Java 开发中最容易出问题的领域。本文总结常见并发问题及其解决方案。

1. 线程安全问题

竞态条件(Race Condition)

多个线程同时读写共享数据,结果取决于执行时序。

public class Counter {
private int count = 0;

// 线程不安全
public void increment() {
count++; // 非原子操作
}
}

解决方案

// 方案1:synchronized
public synchronized void increment() { count++; }

// 方案2:Atomic
private AtomicInteger count = new AtomicInteger();
public void increment() { count.incrementAndGet(); }

// 方案3:LongAdder(高并发)
private LongAdder count = new LongAdder();
public void increment() { count.increment(); }

可见性问题

一个线程修改了变量,其他线程看不到最新值。

public class VisibilityIssue {
private boolean flag = false; // 无volatile

public void writer() {
flag = true;
}

public void reader() {
while (!flag) { // 可能永远循环!
// ...
}
}
}

解决方案

private volatile boolean flag = false;  // 保证可见性

2. 死锁(Deadlock)

互相等待

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

public void methodA() {
synchronized (lock1) {
synchronized (lock2) { // 等待methodB释放lock2
// ...
}
}
}

public void methodB() {
synchronized (lock2) {
synchronized (lock1) { // 等待methodA释放lock1
// ...
}
}
}
}

死锁的四个必要条件

  1. 互斥:资源一次只能被一个线程持有
  2. 占有且等待:持有资源的同时等待其他资源
  3. 不可抢占:资源只能主动释放
  4. 循环等待:形成等待环路

解决方案

// 方案1:统一加锁顺序
public void methodA() {
Object first = lock1.hashCode() < lock2.hashCode() ? lock1 : lock2;
Object second = lock1.hashCode() < lock2.hashCode() ? lock2 : lock1;
synchronized (first) {
synchronized (second) {
// ...
}
}
}

// 方案2:使用tryLock(带超时)
public void safeMethod() {
boolean lock1Acquired = lock1.tryLock(1, TimeUnit.SECONDS);
if (lock1Acquired) {
try {
boolean lock2Acquired = lock2.tryLock(1, TimeUnit.SECONDS);
if (lock2Acquired) {
try {
// ...
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
}

// 方案3:使用并发工具类替代锁
ConcurrentHashMap<String, Object> map = new ConcurrentHashMap<>();

死锁排查

# 1. 找到Java进程
jps -l

# 2. 打印线程堆栈
jstack -l <pid> > thread_dump.txt

# 3. 查找死锁
jstack -l <pid> | grep -A 50 "Found one Java-level deadlock"

3. 活锁(Livelock)

线程不断响应对方的行为,但无法继续执行。

public class Livelock {
private boolean active = false;

public void cooperate(Livelock other) {
while (!active) {
if (other.isActive()) {
// 让对方先执行
doSomething();
} else {
active = true;
}
}
}
}

解决方案:引入随机等待。

Thread.sleep((long)(Math.random() * 100));

4. 饥饿(Starvation)

某些线程长期得不到执行机会。

原因

  • 高优先级线程抢占所有 CPU
  • 非公平锁导致某些线程总是失败

解决方案

// 使用公平锁
ReentrantLock fairLock = new ReentrantLock(true);

// 使用公平信号量
Semaphore fairSemaphore = new Semaphore(10, true);

5. 伪共享(False Sharing)

多个线程修改不同变量,但这些变量在同一缓存行。

public class FalseSharing {
// 同一缓存行(64字节)
volatile long value1;
volatile long value2;
}

// 线程A频繁修改value1
// 线程B频繁修改value2
// 结果:缓存行频繁失效,性能下降

解决方案

// JDK 8+ 使用@Contended
public class PaddedValue {
@sun.misc.Contended
volatile long value1;

@sun.misc.Contended
volatile long value2;
}

// 或手动填充
public class ManualPadding {
long p1, p2, p3, p4, p5, p6, p7; // 填充
volatile long value;
long p8, p9, p10, p11, p12, p13, p14; // 填充
}

6. ThreadLocal 内存泄漏

public class MemoryLeak {
private static final ThreadLocal<byte[]> buffer = new ThreadLocal<>();

public void process() {
buffer.set(new byte[1024 * 1024 * 100]); // 100MB
// ...
// 忘记remove()!
}
}

解决方案

try {
buffer.set(value);
// ...
} finally {
buffer.remove(); // 必须清理
}

7. 并发修改异常

List<String> list = new ArrayList<>();
list.add("a");
list.add("b");

for (String s : list) {
if (s.equals("a")) {
list.remove(s); // ConcurrentModificationException!
}
}

解决方案

// 方案1:使用Iterator
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().equals("a")) {
it.remove();
}
}

// 方案2:使用removeIf(Java 8+)
list.removeIf(s -> s.equals("a"));

// 方案3:使用并发集合
CopyOnWriteArrayList<String> safeList = new CopyOnWriteArrayList<>();

8. 线程池滥用

问题1:无限创建线程

// 错误!每次调用都创建新线程池
public void process() {
ExecutorService executor = Executors.newFixedThreadPool(10);
// ...
}

问题2:使用Executors创建无界队列

// 危险!无界队列会OOM
ExecutorService executor = Executors.newFixedThreadPool(100);

问题3:不关闭线程池

// 错误!线程不回收
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> { ... });
// 没有shutdown()

正确做法

// 使用单例线程池
private static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(
4, 8, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadFactoryBuilder().setNameFormat("pool-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);

// 应用关闭时
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
EXECUTOR.shutdown();
try {
if (!EXECUTOR.awaitTermination(60, TimeUnit.SECONDS)) {
EXECUTOR.shutdownNow();
}
} catch (InterruptedException e) {
EXECUTOR.shutdownNow();
}
}));

9. 并发集合的复合操作

ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();

// 错误!不是原子操作
if (!map.containsKey("key")) {
map.put("key", "value");
}

// 正确
map.putIfAbsent("key", "value");

// 或
map.computeIfAbsent("key", k -> createValue());

10. 总结检查清单

  • 共享变量是否加了 volatile/synchronized/Atomic?
  • 是否存在锁嵌套?是否有死锁风险?
  • ThreadLocal 使用完是否 remove()?
  • 线程池是否有界?是否正确关闭?
  • 集合遍历是否使用了并发修改安全的方式?
  • 复合操作是否使用了原子方法?
  • 是否测试过多线程场景?

并发问题往往难以复现和调试,预防胜于治疗。遵循最佳实践,使用成熟的并发工具类,是避免并发 Bug 的根本之道。


   转载规则


《Java并发编程的常见问题总结》 小乐 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录