Java并发编程:非线程安全计数器的“意外”正确性解析

本文探讨了Java中非线程安全计数器在特定场景下意外返回正确结果的现象。尽管代码存在竞态条件,但由于并发冲突的非确定性、JVM优化和线程调度等因素,其错误行为并非总是立即显现。文章强调,缺乏同步机制的代码不保证在所有环境下都能稳定运行,并提供了示例代码分析,旨在加深对并发编程中线程安全本质的理解。

在Java并发编程中,开发者经常会遇到一个令人困惑的现象:一段明显存在线程安全问题的代码,在运行时却似乎表现“正确”,输出了预期的结果。这往往让初学者误以为代码是安全的,或者对并发编程的理解产生偏差。本文将通过一个经典的非线程安全计数器示例,深入剖析这种“意外”正确性的背后原因,并强调在并发环境中确保线程安全的重要性。

示例代码:非线程安全计数器的实现

我们首先来看一个简单的计数器类及其并发使用示例。Counter 类包含一个私有整型变量 counter,以及一个 incrementCounter 方法用于递增计数。

// Counter.java
public class Counter {

    private int counter = 0;

    public void incrementCounter() {
        // 这是一个非原子操作,通常分解为:
        // 1. 读取 counter 的当前值
        // 2. 将读取到的值加 1
        // 3. 将新值写回 counter
        counter += 1; 
    }

    public int getCounter() {
        return counter;
    }
}

在 Main 类中,我们使用 ExecutorService 启动10个线程,每个线程调用 Counter 对象的 incrementCounter 方法一次。为了最大化竞态条件发生的可能性,我们使用 CountDownLatch 来确保所有线程在同一时刻开始执行计数器递增操作,并在所有线程完成后等待结果。

// Main.java
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        CountDownLatch startSignal = new CountDownLatch(10);
        CountDownLatch doneSignal = new CountDownLatch(10);
        Counter counter = new Counter(); // 共享的非线程安全计数器实例

        for (int i=0; i<10; i++) {
            executorService.submit(() -> {
                try {
                    startSignal.countDown(); // 线程准备就绪,计数器减一
                    startSignal.await();    // 等待所有线程准备就绪后,才开始执行后续代码
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt(); // 重新设置中断标志
                    throw new RuntimeException(e);
                }

                counter.incrementCounter(); // 执行递增操作
                doneSignal.countDown();     // 线程完成,计数器减一
            });
        }

        doneSignal.await(); // 主线程等待所有工作线程完成
        System.out.println("Finished: " + counter.getCounter());
        executorService.shutdownNow(); // 关闭线程池
    }
}

令人惊讶的是,当运行上述代码时,System.out.println 往往会输出 Finished: 10,即正确的计数结果。这与我们对非线程安全代码的预期(通常是丢失更新,导致结果小于10)相悖。

非线程安全代码为何会“意外”正确?

尽管上述代码在并发环境下运行,并且 counter += 1 操作并非原子性的,但它仍然可能在某些情况下返回正确的结果。这并非因为代码本身是线程安全的,而是由于以下几个因素的综合作用:

  1. **竞态条件的非确定性: