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

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免费学习笔记(深入)”;

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

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

喵记多 喵记多

喵记多 – 自带助理的 AI 笔记

喵记多 27 查看详情 喵记多

线程 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,导致减量操作基于一个“过时”的值。

总结与注意事项

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

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

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

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

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

以上就是Java多线程中竞态条件的原理与实践的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/216789.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年11月3日 15:14:19
下一篇 2025年11月3日 15:15:10

相关推荐

  • Remix会话管理:解决开发环境Session不持久化的常见陷阱

    remix应用在开发环境中会话(session)值不持久化,常见原因是`createcookiesessionstorage`配置中`cookie.secure`属性在非https环境下被设置为`true`。本文将深入解析`secure`属性的作用,并提供正确的配置方法,确保会话在本地开发和生产环境…

    2025年12月20日
    000
  • 动态生成与设置模板输入控件值的专业指南

    本文详细介绍了如何利用%ignore_a_1%和html “ 标签,高效地动态生成包含输入框的ui组件,并准确设置其值及其他属性。通过纠正常见的模板内容克隆与元素查找问题,文章提供了清晰的示例代码和最佳实践,确保动态创建的表单元素能够正确初始化并集成到页面中。 在现代Web开发中,动态生…

    2025年12月20日
    000
  • Vue.js集成Firestore:解决collection()方法类型错误

    本文旨在解决Vue.js应用中集成Firebase Firestore时,`collection()`方法报出“Expected first argument to collection() to be a CollectionReference, a DocumentReference or Fi…

    2025年12月20日
    000
  • HTML页面文本内容批量替换为单一字符并保留结构与样式

    本教程详细阐述了如何使用javascript将html页面中所有仅包含文本的元素内容替换为单一字符,同时完整保留页面的原有html结构、元素属性和css样式。核心方法是通过dom遍历识别只包含文本节点的“叶子”元素,并对其文本内容进行修改,确保不触及包含子元素的复杂结构。 在Web开发中,有时我们需…

    2025年12月20日
    000
  • Node.js Express应用中EJS视图渲染失败的排查与解决

    本文旨在解决node.js express应用中ejs模板渲染时遇到的”cannot get /store.html”错误。核心问题在于客户端请求路径与服务器端定义的路由不匹配,以及对ejs视图引擎工作机制的误解。我们将详细分析路由配置、视图引擎设置及正确的访问方式,并提供示…

    2025年12月20日
    000
  • React JSX 列表渲染:深入理解 map 与 forEach 的关键差异

    本文深入探讨react jsx中列表渲染时`map`与`foreach`的关键区别。当需要将数组元素转换为可渲染的jsx组件时,必须使用`map`方法,因为它会返回一个新数组供react渲染。`foreach`仅用于执行副作用,不返回可渲染的值,导致元素无法显示。文章通过代码示例详细阐述正确实践,尤…

    2025年12月20日
    000
  • 如何利用JavaScript的Web Workers进行多线程编程?

    Web Workers是HTML5的API,通过创建后台线程执行耗时任务,避免阻塞主线程;它不能直接操作DOM,需通过postMessage与主线程通信,从而实现JavaScript的多线程并发处理。 JavaScript 是单线程语言,但通过 Web Workers 可以实现多线程编程,避免长时间…

    2025年12月20日
    000
  • Mongoose中ObjectId数组保存空值的排查与修复

    本文深入探讨了mern应用中mongoose模型定义的一个常见问题:当尝试将用户id数组保存到`conversation`模型的`members`字段时,数据却显示为空值。文章分析了错误的schema定义,并提供了将`objectid`数组正确定义为`type: [mongoose.schema.t…

    2025年12月20日
    000
  • 使用 JavaScript 和 ApexCharts 实现数据动态追加

    本文将介绍如何使用 JavaScript 和 ApexCharts 库,在指定的时间间隔内动态地向图表中追加数据。我们将通过一个具体的示例,演示如何在点击按钮后,每隔 2 秒向柱状图中添加新的数据,并探讨实现过程中需要注意的关键点。 动态追加数据的实现 要实现数据的动态追加,核心在于使用 setIn…

    2025年12月20日
    000
  • Vitejs项目HTML文件加载错误:路径中特殊字符的排查与解决

    在vite/vue项目开发中,开发者可能会遇到“no loader is configured for “.html” files”的错误,尤其是在多项目解决方案中。尽管错误信息指向html加载器配置缺失,但常见且隐蔽的原因是项目文件路径中包含特殊字符,例如`#`。本文将深入…

    2025年12月20日
    000
  • 如何利用Generator函数实现复杂的异步流程控制?

    Generator 函数通过 yield 暂停执行,结合 Promise 实现异步流程控制,支持串行、并行、条件分支与错误重试,如使用 run 执行器处理 yield 返回的 Promise,实现同步式异步代码。 Generator 函数通过暂停和恢复执行的能力,为异步流程控制提供了更直观的编码方式…

    2025年12月20日
    000
  • JavaScript实现多图片本地存储与动态展示教程

    本教程将指导您如何使用javascript从文件输入中获取多张图片,并将其以数组形式存储到浏览器的本地存储(localstorage)中。通过filereader api读取图片数据,并动态渲染这些图片,构建一个基础的图片展示区域,为实现图片滑块功能奠定基础。文章涵盖了从数据捕获、持久化存储到动态显…

    2025年12月20日 好文分享
    000
  • 如何优化JavaScript包的体积与加载性能?

    答案:前端JS性能优化需减小包体积、按需加载、提升执行效率。通过Tree Shaking、代码压缩、避免全量引入减小体积;利用动态import、SplitChunks实现代码分割与懒加载;使用async/defer、preload、Gzip、缓存提升加载效率;结合Bundle分析、体积告警、运行时监…

    2025年12月20日
    000
  • JavaScript对象属性计算:利用Getter实现动态值

    本文探讨了如何在JavaScript对象中,基于其他属性的值动态计算并获取一个新属性的值,同时避免函数调用语法。通过详细分析直接函数和立即执行函数表达式(IIFE)的局限性,文章重点介绍了JavaScript的`getter`语法作为优雅的解决方案,展示了如何使用它来实现属性的按需计算和无缝访问,提…

    2025年12月20日
    000
  • 解决Electron-vite预览时白屏问题:HashRouter的妙用

    本文旨在解决electron-vite项目在`vite preview`时出现的白屏问题,尽管构建过程成功。核心原因在于react应用中`browserrouter`与electron或静态预览环境的兼容性冲突。教程将详细阐述为何应将`browserrouter`替换为`hashrouter`,并提…

    2025年12月20日
    000
  • JavaScript Canvas 坐标变换与元素旋转指南

    本教程详细介绍了如何使用JavaScript的HTML Canvas API实现图形元素的旋转。我们将深入探讨Canvas上下文的保存与恢复、坐标系的平移与旋转等核心变换操作,并通过具体代码示例演示如何围绕元素中心进行旋转,以及如何将这些技术应用于图像和文本,帮助开发者高效地在Canvas上创建动态…

    2025年12月20日
    000
  • 使用 apicache-plus 精准管理和清除路由缓存

    本文旨在解决 MERN 应用中 `apicache` 路由缓存清除不生效的问题。通过引入 `apicache-plus` 包,并利用其缓存分组(`apicacheGroup`)功能,开发者可以实现对特定路由缓存的精准管理和清除,确保数据更新后能立即反映在用户界面,从而提升应用的响应性和数据一致性。 …

    2025年12月20日
    000
  • 构建可持久化多图上传与动态展示教程

    本教程将详细介绍如何使用javascript实现多张图片的文件上传、将其转换为base64格式并存储到浏览器的`localstorage`中,最后动态地在网页上展示这些图片,为构建图片画廊或简易轮播图奠定基础。 一、 引言:多图片处理的需求 在现代Web应用中,用户上传图片并进行展示是一个常见的功能…

    2025年12月20日 好文分享
    000
  • 使用useReducer和优化数据结构管理React中的嵌套对象数组

    本文将探讨在react应用中如何高效地更新嵌套在对象内部的数组(包含多个对象)的状态。针对使用`usestate`可能遇到的复杂性,我们将介绍如何利用`usereducer`钩子来管理复杂状态,并通过优化数据结构(将数组转换为映射)来简化数据读写操作,从而提升状态管理的清晰度和性能。 挑战:Reac…

    2025年12月20日
    000
  • Vite + React 项目中正确导入静态图片资源的方法

    在 vite 与 react 项目中,直接通过命名导出导入图片等静态资源可能导致“uncaught syntaxerror: ambiguous indirect export”错误。本文将详细介绍如何利用 `new url(path, import.meta.url).href` 这一标准 web…

    2025年12月20日 好文分享
    000

发表回复

登录后才能评论
关注微信