并发下的 “灵异数值”:i++ 不是原子操作

67 阅读2分钟

一、Bug 场景

在一个多线程的 Java 应用程序中,有一个任务需要对一个整数进行多次递增操作,并且在不同线程中并发执行该任务。开发人员认为简单的 i++ 操作能够正确地对整数进行递增,然而在实际运行过程中,却得到了与预期不符的结果,出现了一些 “灵异数值”,导致程序的业务逻辑出现错误。

二、代码示例

计数器类(有缺陷)

public class Counter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

线程任务类

public class IncrementTask implements Runnable {
    private Counter counter;

    public IncrementTask(Counter counter) {
        this.counter = counter;
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            counter.increment();
        }
    }
}

测试代码

public class ConcurrencyBugExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread[] threads = new Thread[10];

        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(new IncrementTask(counter));
            threads[i].start();
        }

        for (Thread thread : threads) {
            thread.join();
        }

        System.out.println("预期结果应为 10000,实际结果: " + counter.getCount());
    }
}

三、问题描述

  1. 预期行为:10 个线程每个线程执行 1000 次 count++ 操作,最终 count 的值应该为 10000。
  2. 实际行为:每次运行程序,得到的 count 值都小于 10000,而且每次结果都不一致。这是因为 i++ 操作在并发环境下不是原子操作。i++ 实际上包含了三个步骤:读取 i 的值、将值加 1、将加 1 后的值写回内存。在多线程环境下,当一个线程读取了 i 的值,但还未将加 1 后的值写回内存时,另一个线程也读取了 i 的值,这样就会导致两个线程对同一个值进行加 1 操作,从而出现数据竞争,最终结果小于预期值。

四、解决方案

  1. 使用 synchronized 关键字:通过 synchronized 关键字来同步方法,确保同一时间只有一个线程能够执行 increment 方法。
public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}
  1. 使用 AtomicIntegerAtomicInteger 类提供了原子性的递增操作,能有效避免数据竞争问题。
import java.util.concurrent.atomic.AtomicInteger;

public class Counter {
    private AtomicInteger count = new AtomicInteger(0);

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

    public int getCount() {
        return count.get();
    }
}
  1. 使用 Lock 接口:例如 ReentrantLock,它提供了比 synchronized 更灵活的锁机制。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private int count = 0;
    private Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}