Java 多线程:3 个常见错误让程序崩溃(附解决方案)

Java 多线程编程能显著提升程序性能,但也容易引入隐蔽的错误,导致程序崩溃或行为异常。本文将分析三个最常见的错误——竞态条件、死锁和内存可见性问题,并提供实用的解决方案。每个错误都配有代码示例和修复建议,帮助您避免这些陷阱。


错误 1: 竞态条件 (Race Condition)

竞态条件发生在多个线程同时访问和修改共享数据时,导致数据不一致或程序崩溃。例如,两个线程同时增加一个计数器,可能因非原子操作而丢失更新。

错误示例代码

public class RaceConditionExample {
    private int counter = 0;

    public void increment() {
        counter++; // 非原子操作,可能导致问题
    }

    public static void main(String[] args) throws InterruptedException {
        RaceConditionExample example = new RaceConditionExample();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("Final counter: " + example.counter); // 可能输出小于 2000
    }
}

原因分析
counter++ 操作不是原子的,涉及读取、修改和写入。多个线程同时执行时,可能发生覆盖,导致计数器值错误。

解决方案
使用 synchronized 关键字或 AtomicInteger 确保原子性。

// 修复方案:使用 synchronized
public synchronized void increment() {
    counter++;
}

// 或使用 AtomicInteger(更高效)
import java.util.concurrent.atomic.AtomicInteger;

public class FixedRaceCondition {
    private AtomicInteger counter = new AtomicInteger(0);

    public void increment() {
        counter.incrementAndGet();
    }
}

关键点

  • 优先使用 java.util.concurrent.atomic 包中的原子类。
  • 避免过度同步以减少性能开销。

错误 2: 死锁 (Deadlock)

死锁是指两个或多个线程相互等待对方释放资源,导致程序永久停滞。常见于嵌套锁或资源顺序不一致时。

错误示例代码

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

    public void method1() {
        synchronized (lock1) {
            System.out.println("Acquired lock1");
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            synchronized (lock2) {
                System.out.println("Acquired lock2");
            }
        }
    }

    public void method2() {
        synchronized (lock2) {
            System.out.println("Acquired lock2");
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            synchronized (lock1) {
                System.out.println("Acquired lock1");
            }
        }
    }

    public static void main(String[] args) {
        DeadlockExample example = new DeadlockExample();
        new Thread(example::method1).start();
        new Thread(example::method2).start(); // 可能死锁
    }
}

原因分析
method1method2 以相反顺序获取锁(lock1 和 lock2)。当线程 A 持有 lock1 并等待 lock2,线程 B 持有 lock2 并等待 lock1 时,就会死锁。

解决方案
统一锁获取顺序或使用 java.util.concurrent.locks.Lock 的超时机制。

// 修复方案:统一锁顺序
public void fixedMethod1() {
    synchronized (lock1) {
        synchronized (lock2) {
            // 安全操作
        }
    }
}

public void fixedMethod2() {
    synchronized (lock1) { // 与 method1 相同顺序
        synchronized (lock2) {
            // 安全操作
        }
    }
}

// 或使用 ReentrantLock 带超时
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

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

    public void safeMethod() {
        while (true) {
            if (lock1.tryLock()) {
                try {
                    if (lock2.tryLock()) {
                        try {
                            // 安全操作
                            break;
                        } finally {
                            lock2.unlock();
                        }
                    }
                } finally {
                    lock1.unlock();
                }
            }
            try { Thread.sleep(10); } catch (InterruptedException e) {}
        }
    }
}

关键点

  • 始终按固定顺序获取多个锁。
  • 使用 tryLock() 设置超时,避免无限等待。

错误 3: 内存可见性问题 (Memory Visibility Issue)

内存可见性问题是指一个线程修改共享变量后,其他线程无法立即看到变化,导致数据过时或程序崩溃。常见于未使用 volatile 或不当同步时。

错误示例代码

public class VisibilityIssueExample {
    private boolean flag = false; // 未使用 volatile,可能导致可见性问题

    public void start() {
        new Thread(() -> {
            while (!flag) {
                // 空循环,可能永远不退出
            }
            System.out.println("Flag changed detected");
        }).start();

        try { Thread.sleep(1000); } catch (InterruptedException e) {}
        flag = true; // 主线程修改 flag
        System.out.println("Flag set to true");
    }

    public static void main(String[] args) {
        new VisibilityIssueExample().start(); // 子线程可能无法检测到 flag 变化
    }
}

原因分析
由于 CPU 缓存或编译器优化,子线程可能读取到 flag 的旧值(false),导致无限循环,而主线程的修改(true)不可见。

解决方案
使用 volatile 关键字或 synchronized 确保变量的可见性。

// 修复方案:使用 volatile
public class FixedVisibilityIssue {
    private volatile boolean flag = false; // 确保修改对所有线程立即可见

    public void start() {
        new Thread(() -> {
            while (!flag) {}
            System.out.println("Flag changed detected");
        }).start();

        try { Thread.sleep(1000); } catch (InterruptedException e) {}
        flag = true;
        System.out.println("Flag set to true");
    }
}

// 或使用 synchronized 方法
public class AlternativeFix {
    private boolean flag = false;

    public synchronized void setFlag(boolean value) {
        flag = value;
    }

    public synchronized boolean getFlag() {
        return flag;
    }
}

关键点

  • volatile 适用于单个变量的可见性,但不保证原子性(如复合操作)。
  • 对于复杂场景,使用 synchronizedjava.util.concurrent 工具类。

总结

多线程编程中的错误往往难以调试,但通过理解竞态条件、死锁和内存可见性问题,您可以有效预防程序崩溃:

  • 竞态条件:使用原子类或同步机制保护共享数据。
  • 死锁:统一锁获取顺序或使用超时锁。
  • 内存可见性问题:优先使用 volatile 或同步方法。 最佳实践包括:优先使用 java.util.concurrent 包(如 ExecutorService)、避免过度同步、并进行多线程测试(如用 CountDownLatch)。通过这些方案,您的 Java 多线程程序将更健壮高效。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐