0

0

深入理解Java多线程中的竞态条件与非原子操作

花韻仙語

花韻仙語

发布时间:2025-09-01 13:38:28

|

221人浏览过

|

来源于php中文网

原创

深入理解Java多线程中的竞态条件与非原子操作

本教程旨在深入探讨Java多线程编程中的竞态条件。通过分析一个未能成功复现竞态条件的初始案例,并引入一个精心设计的示例,我们将清晰地演示共享可变状态、非原子操作如何导致数据不一致。文章将详细解释竞态条件的产生机制,并提供代码示例及输出分析,帮助开发者理解并识别这类并发问题。

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)。原因在于:

  1. 无共享可变状态的并发修改:MyThread类中的sum变量是每个MyThread实例的成员变量。当executor.execute(threadX)被调用时,每个线程(或任务)都持有一个独立的MyThread对象实例。因此,thread1修改的是thread1.sum,thread2修改的是thread2.sum,它们之间互不影响。num数组虽然是共享的,但线程只读取其中的元素(如果被初始化),并未并发地对其进行修改。
  2. 结果聚合时序:主线程在调用executor.shutdown()后,会通过while (!executor.isTerminated()) {}循环等待所有子线程的任务执行完毕。这意味着threadX.getSum()方法总是在每个线程完成其run()方法并计算出最终sum值之后才被调用。因此,最终的总和计算是基于每个线程独立且已完成的局部结果,不存在并发访问和修改的问题。

综上所述,由于缺乏共享的可变状态被多个线程同时修改,这段代码不会引发竞态条件。

2. 揭示竞态条件:共享资源的非原子操作

要真正演示竞态条件,我们需要构造一个场景,其中多个线程并发地修改同一个共享的可变资源,并且这些修改操作不是原子性的。原子操作是指一个操作在执行过程中不会被中断,要么全部完成,要么全部不执行。像counter++或counter--这样的简单操作,在底层实际上包含三个步骤:

  1. 读取:将counter的当前值从内存读入CPU寄存器。
  2. 修改:在寄存器中对值进行加1或减1操作。
  3. 写入:将寄存器中的新值写回内存中的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();
    }
}

在这个示例中:

SoftGist
SoftGist

SoftGist是一个软件工具目录站,每天为您带来最好、最令人兴奋的软件新产品。

下载
  • 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

分析上述输出,我们可以观察到明显的竞态条件迹象:

  1. “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实际上是所有线程共同作用的结果,而不是它单独递增的结果。
  2. 线程执行交错:输出顺序表明线程的执行是高度交错的。例如,在所有线程都打印完“After increment”之后,才开始有线程打印“at last”。这说明在某个时间点,所有5个线程可能都完成了increment()方法中Thread.sleep()之后的counter++操作,使得counter的值达到了5。然后,它们才陆续执行decrement()操作。
  3. 最终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提供的并发工具来防范它们,是编写健壮、高效多线程程序的关键。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
while的用法
while的用法

while的用法是“while 条件: 代码块”,条件是一个表达式,当条件为真时,执行代码块,然后再次判断条件是否为真,如果为真则继续执行代码块,直到条件为假为止。本专题为大家提供while相关的文章、下载、课程内容,供大家免费下载体验。

94

2023.09.25

string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

443

2023.08.02

int占多少字节
int占多少字节

int占4个字节,意味着一个int变量可以存储范围在-2,147,483,648到2,147,483,647之间的整数值,在某些情况下也可能是2个字节或8个字节,int是一种常用的数据类型,用于表示整数,需要根据具体情况选择合适的数据类型,以确保程序的正确性和性能。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

544

2024.08.29

c++怎么把double转成int
c++怎么把double转成int

本专题整合了 c++ double相关教程,阅读专题下面的文章了解更多详细内容。

93

2025.08.29

C++中int的含义
C++中int的含义

本专题整合了C++中int相关内容,阅读专题下面的文章了解更多详细内容。

197

2025.08.29

硬盘接口类型介绍
硬盘接口类型介绍

硬盘接口类型有IDE、SATA、SCSI、Fibre Channel、USB、eSATA、mSATA、PCIe等等。详细介绍:1、IDE接口是一种并行接口,主要用于连接硬盘和光驱等设备,它主要有两种类型:ATA和ATAPI,IDE接口已经逐渐被SATA接口;2、SATA接口是一种串行接口,相较于IDE接口,它具有更高的传输速度、更低的功耗和更小的体积;3、SCSI接口等等。

1126

2023.10.19

PHP接口编写教程
PHP接口编写教程

本专题整合了PHP接口编写教程,阅读专题下面的文章了解更多详细内容。

192

2025.10.17

php8.4实现接口限流的教程
php8.4实现接口限流的教程

PHP8.4本身不内置限流功能,需借助Redis(令牌桶)或Swoole(漏桶)实现;文件锁因I/O瓶颈、无跨机共享、秒级精度等缺陷不适用高并发场景。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

1622

2025.12.29

俄罗斯Yandex引擎入口
俄罗斯Yandex引擎入口

2026年俄罗斯Yandex搜索引擎最新入口汇总,涵盖免登录、多语言支持、无广告视频播放及本地化服务等核心功能。阅读专题下面的文章了解更多详细内容。

158

2026.01.28

热门下载

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

精品课程

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

共23课时 | 3万人学习

C# 教程
C# 教程

共94课时 | 7.8万人学习

Java 教程
Java 教程

共578课时 | 52.6万人学习

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

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