0

0

Java多线程中竞态条件的原理与实践

霞舞

霞舞

发布时间:2025-09-01 14:26:01

|

412人浏览过

|

来源于php中文网

原创

Java多线程中竞态条件的原理与实践

本文深入探讨了Java多线程编程中的竞态条件(Race Condition),通过分析一个未能产生竞态条件的求和示例,引出并详细演示了如何通过共享可变状态和非原子操作来故意制造竞态条件。文章提供了具体的Java代码示例,解释了竞态条件发生的原因、其在输出中的体现,并强调了在并发编程中识别和避免此类问题的必要性。

理解竞态条件:从“无意安全”到“有意危险”

在多线程编程中,竞态条件(race condition)是一个常见的并发问题,它指的是多个线程以不可预测的顺序访问和修改共享资源时,导致程序执行结果依赖于特定线程的执行顺序,从而产生不正确或不可预测的结果。理解竞态条件对于编写健壮的并发应用至关重要。

初始尝试:为何求和操作未触发竞态条件?

考虑一个常见的场景:使用多线程计算一个大数组的总和。一个初学者可能会编写出如下代码,期望它能展示竞态条件,但结果却总是正确的。

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() {
        // 数组num在此处被初始化,但其元素在MyThread的run方法中并未被修改,仅用于构造器。
        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); // 结果总是500500
    }

    private static class MyThread implements Runnable {
        private int[] num; // 数组num在此处作为成员变量,但其元素在run方法中并未被修改。
        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 实例私有的成员变量。每个线程在 run() 方法中累加的 sum 都是它自己独有的,不会与其他线程的 sum 变量发生冲突。虽然 int[] num 数组被所有 MyThread 实例共享,但在 run() 方法中,它并未被修改,仅仅是在构造函数中被引用。因此,这里不存在多个线程同时修改同一个共享变量的情况,自然也就不会出现竞态条件。

制造竞态条件:共享可变状态与非原子操作

要真正观察到竞态条件,我们需要确保多个线程同时访问并修改一个共享的、可变的资源,并且这些修改操作不是原子性的。以下是一个演示竞态条件的典型示例:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class RaceConditionDemo implements Runnable {
    private int counter = 0; // 共享的、可变的资源

    public void increment() {
        try {
            // 引入短暂延迟,增加线程上下文切换的可能性,从而更容易暴露竞态条件
            Thread.sleep(10); 
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        counter++; // 非原子操作:读取-修改-写入
    }

    public void decrement() {
        counter--; // 非原子操作:读取-修改-写入
    }

    public int getValue() {
        return counter;
    }

    @Override
    public void run() {
        this.increment();
        System.out.println("线程 " + Thread.currentThread().getName() + " 增量后值: " + this.getValue());

        this.decrement();
        System.out.println("线程 " + Thread.currentThread().getName() + " 最终值: " + this.getValue());
    }

    public static void main(String args[]) {
        RaceConditionDemo counterInstance = new RaceConditionDemo(); // 共享同一个实例
        ExecutorService executor = Executors.newFixedThreadPool(5);

        for (int i = 0; i < 5; i++) {
            executor.execute(new Thread(counterInstance, "Thread-" + (i + 1)));
        }

        executor.shutdown();
        while (!executor.isTerminated()) {
            // 等待所有任务完成
        }
        System.out.println("所有线程执行完毕,最终计数器值: " + counterInstance.getValue());
    }
}

代码分析与竞态条件表现:

立即学习Java免费学习笔记(深入)”;

  1. 共享资源: counter 变量是 RaceConditionDemo 类的成员变量,并且在 main 方法中,所有 Thread 实例都共享同一个 counterInstance 对象。这意味着所有线程都在操作同一个 counter 变量。
  2. 非原子操作: counter++ 和 counter-- 看起来是简单的单行代码,但它们并非原子操作。在底层,counter++ 通常包含以下三个步骤:
    • 读取 counter 的当前值。
    • 将读取到的值加1。
    • 将新值写回 counter。 counter-- 同理。
  3. 时序不确定性: Thread.sleep(10) 方法的引入,虽然是为了模拟实际工作中的延迟,但更重要的是,它增加了线程在执行 counter++ 或 counter-- 的中间步骤时发生上下文切换的可能性。 例如,线程A读取了 counter 的值(假设为0),正要执行加1操作时,操作系统可能切换到线程B。线程B也读取了 counter 的值(仍然是0),然后执行加1并写回(counter 变为1)。接着,线程A恢复执行,它会使用之前读取到的旧值(0)进行加1,然后写回(counter 变为1)。最终结果是 counter 只有一次增量,而不是两次。

示例输出(每次运行可能不同):

白果AI论文
白果AI论文

论文AI生成学术工具,真实文献,免费不限次生成论文大纲 10 秒生成逻辑框架,10 分钟产出初稿,智能适配 80+学科。支持嵌入图表公式与合规文献引用

下载
线程 Thread-3 增量后值: 5
线程 Thread-5 增量后值: 5
线程 Thread-1 增量后值: 5
线程 Thread-2 增量后值: 5
线程 Thread-4 增量后值: 5
线程 Thread-2 最终值: 1
线程 Thread-1 最终值: 2
线程 Thread-5 最终值: 3
线程 Thread-3 最终值: 4
线程 Thread-4 最终值: 0
所有线程执行完毕,最终计数器值: 0 

从上述输出中,我们可以观察到:

  • 乱序输出: 增量后值 和 最终值 的打印顺序是混乱的,表明线程的执行是交错的。
  • 不一致的值: 多个线程可能同时打印出相同的 增量后值(例如,所有线程都打印5),这说明在某个线程完成 increment 操作并打印值之前,其他线程可能已经修改了 counter。
  • 最终结果不确定: 如果每个线程都执行一次 increment 和一次 decrement,理想情况下 counter 的最终值应该是 0(初始0 + 5次增量 - 5次减量)。然而,由于竞态条件,最终输出的 counterInstance.getValue() 可能是 0,也可能是其他值(例如,示例中最终是0,但多次运行可能会得到非0值)。这是因为在某个线程执行 decrement 之前,另一个线程可能已经修改了 counter,导致减量操作基于一个“过时”的值。

总结与注意事项

竞态条件是并发编程中的一个核心挑战。它发生在:

  1. 存在共享的可变状态: 多个线程访问同一个变量或数据结构。
  2. 存在非原子操作: 对共享状态的修改操作不是一步完成的,可以被其他线程打断。

为了避免竞态条件,开发者需要采取适当的同步机制,确保在同一时刻只有一个线程能够访问和修改共享资源。常见的解决方案包括:

  • synchronized 关键字: 用于方法或代码块,提供内置的锁机制。
  • java.util.concurrent.locks.Lock 接口: 提供更灵活的锁控制,如 ReentrantLock。
  • java.util.concurrent.atomic 包下的原子类: 例如 AtomicInteger,它们提供了对基本数据类型和引用类型进行原子操作的方法,无需显式加锁。
  • 不可变对象: 如果共享对象是不可变的,那么多个线程同时访问它就不会产生竞态条件,因为它们无法修改它。

理解并能够识别竞态条件是编写正确、高效并发程序的关键一步。通过上述示例,我们不仅了解了竞态条件的表现形式,更重要的是,理解了其产生的根本原因。

相关专题

更多
java
java

Java是一个通用术语,用于表示Java软件及其组件,包括“Java运行时环境 (JRE)”、“Java虚拟机 (JVM)”以及“插件”。php中文网还为大家带了Java相关下载资源、相关课程以及相关文章等内容,供大家免费下载使用。

832

2023.06.15

java正则表达式语法
java正则表达式语法

java正则表达式语法是一种模式匹配工具,它非常有用,可以在处理文本和字符串时快速地查找、替换、验证和提取特定的模式和数据。本专题提供java正则表达式语法的相关文章、下载和专题,供大家免费下载体验。

737

2023.07.05

java自学难吗
java自学难吗

Java自学并不难。Java语言相对于其他一些编程语言而言,有着较为简洁和易读的语法,本专题为大家提供java自学难吗相关的文章,大家可以免费体验。

733

2023.07.31

java配置jdk环境变量
java配置jdk环境变量

Java是一种广泛使用的高级编程语言,用于开发各种类型的应用程序。为了能够在计算机上正确运行和编译Java代码,需要正确配置Java Development Kit(JDK)环境变量。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

397

2023.08.01

java保留两位小数
java保留两位小数

Java是一种广泛应用于编程领域的高级编程语言。在Java中,保留两位小数是指在进行数值计算或输出时,限制小数部分只有两位有效数字,并将多余的位数进行四舍五入或截取。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

398

2023.08.02

java基本数据类型
java基本数据类型

java基本数据类型有:1、byte;2、short;3、int;4、long;5、float;6、double;7、char;8、boolean。本专题为大家提供java基本数据类型的相关的文章、下载、课程内容,供大家免费下载体验。

446

2023.08.02

java有什么用
java有什么用

java可以开发应用程序、移动应用、Web应用、企业级应用、嵌入式系统等方面。本专题为大家提供java有什么用的相关的文章、下载、课程内容,供大家免费下载体验。

430

2023.08.02

java在线网站
java在线网站

Java在线网站是指提供Java编程学习、实践和交流平台的网络服务。近年来,随着Java语言在软件开发领域的广泛应用,越来越多的人对Java编程感兴趣,并希望能够通过在线网站来学习和提高自己的Java编程技能。php中文网给大家带来了相关的视频、教程以及文章,欢迎大家前来学习阅读和下载。

16925

2023.08.03

php与html混编教程大全
php与html混编教程大全

本专题整合了php和html混编相关教程,阅读专题下面的文章了解更多详细内容。

3

2026.01.13

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 2.5万人学习

C# 教程
C# 教程

共94课时 | 6.6万人学习

Java 教程
Java 教程

共578课时 | 45.5万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号