
1. 多线程求和案例分析:为何没有竞态条件?
在多线程编程中,竞态条件(race condition)是由于多个线程并发访问和修改共享资源而导致程序执行结果不确定的现象。然而,并非所有多线程场景都会自然产生竞态条件。考虑以下一个尝试使用多线程计算数组和的java代码片段:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SyncDemo1 {
public static void main(String[] args) {
new SyncDemo1().startThread();
}
private void startThread() {
int[] num = new int[1000]; // 数组在此处未初始化,但对本例影响不大
ExecutorService executor = Executors.newFixedThreadPool(5);
MyThread thread1 = new MyThread(num, 1, 200);
MyThread thread2 = new MyThread(num, 201, 400);
MyThread thread3 = new MyThread(num, 401, 600);
MyThread thread4 = new MyThread(num, 601, 800);
MyThread thread5 = new MyThread(num, 801, 1000);
executor.execute(thread1);
executor.execute(thread2);
executor.execute(thread3);
executor.execute(thread4);
executor.execute(thread5);
executor.shutdown();
while (!executor.isTerminated()) {
// 等待所有任务完成
}
int totalSum = thread1.getSum() + thread2.getSum() + thread3.getSum() + thread4.getSum() + thread5.getSum();
System.out.println(totalSum);
}
private static class MyThread implements Runnable {
private int[] num; // 数组本身不是共享修改的目标
private int from, to, sum; // sum是每个MyThread实例的局部变量
public MyThread(int[] num, int from, int to) {
this.num = num;
this.from = from;
this.to = to;
sum = 0;
}
public void run() {
for (int i = from; i <= to; i++) {
sum += i; // 每个线程修改的是自己的sum变量
}
// 模拟耗时操作,但不影响sum的计算
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public int getSum() {
return this.sum;
}
}
}尽管这段代码使用了多线程,但它并不会产生竞态条件,每次运行都会得到正确的结果(如果数组初始化为1到1000,则总和为500500)。原因在于:
- 无共享可变状态的并发修改:MyThread类中的sum变量是每个MyThread实例的成员变量。当executor.execute(threadX)被调用时,每个线程(或任务)都持有一个独立的MyThread对象实例。因此,thread1修改的是thread1.sum,thread2修改的是thread2.sum,它们之间互不影响。num数组虽然是共享的,但线程只读取其中的元素(如果被初始化),并未并发地对其进行修改。
- 结果聚合时序:主线程在调用executor.shutdown()后,会通过while (!executor.isTerminated()) {}循环等待所有子线程的任务执行完毕。这意味着threadX.getSum()方法总是在每个线程完成其run()方法并计算出最终sum值之后才被调用。因此,最终的总和计算是基于每个线程独立且已完成的局部结果,不存在并发访问和修改的问题。
综上所述,由于缺乏共享的可变状态被多个线程同时修改,这段代码不会引发竞态条件。
2. 揭示竞态条件:共享资源的非原子操作
要真正演示竞态条件,我们需要构造一个场景,其中多个线程并发地修改同一个共享的可变资源,并且这些修改操作不是原子性的。原子操作是指一个操作在执行过程中不会被中断,要么全部完成,要么全部不执行。像counter++或counter--这样的简单操作,在底层实际上包含三个步骤:
- 读取:将counter的当前值从内存读入CPU寄存器。
- 修改:在寄存器中对值进行加1或减1操作。
- 写入:将寄存器中的新值写回内存中的counter。
当多个线程并发执行这些非原子操作时,如果线程执行的步骤发生交错,就可能导致数据丢失或不一致。
立即学习“Java免费学习笔记(深入)”;
3. 竞态条件演示代码
以下代码示例通过一个共享的int类型计数器来演示竞态条件。int是基本类型,非线程安全,其自增/自减操作是非原子的。为了增加竞态条件发生的概率,我们在increment()方法中引入了Thread.sleep()来模拟耗时操作,使得线程切换更容易发生在非原子操作的中间。
import java.util.concurrent.TimeUnit;
class RaceConditionDemo implements Runnable {
private int counter = 0; // 共享的计数器
public void increment() {
try {
// 模拟耗时操作,增加线程切换的可能性
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
counter++; // 非原子操作
}
public void decrement() {
counter--; // 非原子操作
}
public int getValue() {
return counter;
}
@Override
public void run() {
this.increment();
System.out.println("Value for Thread After increment "
+ Thread.currentThread().getName() + " " + this.getValue());
this.decrement();
System.out.println("Value for Thread at last "
+ Thread.currentThread().getName() + " " + this.getValue());
}
public static void main(String args[]) {
RaceConditionDemo counter = new RaceConditionDemo(); // 多个线程共享同一个RaceConditionDemo实例
Thread t1 = new Thread(counter, "Thread-1");
Thread t2 = new Thread(counter, "Thread-2");
Thread t3 = new Thread(counter, "Thread-3");
Thread t4 = new Thread(counter, "Thread-4");
Thread t5 = new Thread(counter, "Thread-5");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}在这个示例中:
- counter是RaceConditionDemo类的一个成员变量,并且只有一个RaceConditionDemo实例被所有线程共享。
- increment()和decrement()方法直接操作这个共享的counter变量。
- increment()中的Thread.sleep(10)使得一个线程在执行counter++的“读-修改-写”过程中,有更大的机会被调度器中断,从而让其他线程有机会执行其操作。
4. 输出结果分析
运行RaceConditionDemo类多次,你会发现每次的输出顺序和最终的counter值都可能不同。以下是一个可能的运行输出示例:
Value for Thread After increment Thread-3 5 Value for Thread After increment Thread-5 5 Value for Thread After increment Thread-1 5 Value for Thread After increment Thread-2 5 Value for Thread at last Thread-2 1 Value for Thread After increment Thread-4 5 Value for Thread at last Thread-1 2 Value for Thread at last Thread-5 3 Value for Thread at last Thread-3 4 Value for Thread at last Thread-4 0
分析上述输出,我们可以观察到明显的竞态条件迹象:
- “After increment”的值不一致:理想情况下,如果操作是原子的,当一个线程完成increment()并打印“After increment”时,counter的值应该是1(因为它只被自己递增了一次)。但我们看到,多个线程打印出的“After increment”的值都是5。这表明在某个线程完成increment()之前,其他线程已经读取并递增了counter。例如,当Thread-3执行increment()时,它可能读取到counter为0,但在它将counter写回5之前,其他四个线程也完成了increment()操作,导致counter的值达到了5。当Thread-3最终完成写入时,它可能将5写回,但这个5实际上是所有线程共同作用的结果,而不是它单独递增的结果。
- 线程执行交错:输出顺序表明线程的执行是高度交错的。例如,在所有线程都打印完“After increment”之后,才开始有线程打印“at last”。这说明在某个时间点,所有5个线程可能都完成了increment()方法中Thread.sleep()之后的counter++操作,使得counter的值达到了5。然后,它们才陆续执行decrement()操作。
- 最终counter值的不确定性:理论上,5个线程各自执行一次increment()和一次decrement(),最终counter的值应该是 0 + 5 - 5 = 0。然而,在上述输出中,Thread-4打印的“at last”值为0,但这并不代表最终的counter值是0,因为其他线程可能还在执行。实际上,由于竞态条件,最终counter的实际值是不可预测的。在某些运行中,它可能确实是0,但在另一些运行中,它可能是其他任意值。
这种不可预测性和数据不一致性正是竞态条件的核心特征。
5. 总结与防范
竞态条件是多线程编程中一个普遍且难以调试的问题。它发生在多个线程尝试同时访问和修改共享资源,并且操作顺序无法预测时。为了避免竞态条件,确保数据的一致性和程序的正确性,我们需要采取适当的同步机制:
- synchronized关键字:可以用于方法或代码块,确保在任何给定时刻只有一个线程可以执行被synchronized修饰的代码。
- java.util.concurrent.locks.Lock接口:提供了比synchronized更灵活的锁定机制,例如可重入锁ReentrantLock,支持尝试获取锁、定时获取锁等高级功能。
- java.util.concurrent.atomic包下的原子类:如AtomicInteger、AtomicLong等,它们提供了对基本类型和引用类型的原子操作,内部通过CAS(Compare-And-Swap)等无锁机制实现,效率通常高于synchronized。
- 不可变对象:设计不可变对象可以从根本上消除竞态条件,因为不可变对象一旦创建就不能被修改,也就没有了共享可变状态的问题。
理解竞态条件的产生机制,并熟练运用Java提供的并发工具来防范它们,是编写健壮、高效多线程程序的关键。









