0

0

深入理解Java并发编程:非线程安全代码为何有时“看似”正确

DDD

DDD

发布时间:2025-10-23 13:34:12

|

465人浏览过

|

来源于php中文网

原创

深入理解java并发编程:非线程安全代码为何有时“看似”正确

本文探讨了Java中非线程安全代码在特定条件下可能意外地产生正确结果的现象。通过分析一个多线程计数器示例,文章解释了这种“偶然正确性”背后的原因,包括JVM、JIT编译器和硬件的优化与调度不确定性,以及Java内存模型的影响。强调了非线程安全代码缺乏行为保证的本质,并提供了使用`AtomicInteger`等机制构建真正线程安全计数器的专业解决方案,旨在纠正对并发编程的常见误解。

深入理解Java并发编程:非线程安全代码为何有时“看似”正确

在Java并发编程中,线程安全是一个核心概念。当多个线程同时访问和修改共享数据时,如果不采取适当的同步措施,就可能发生竞态条件(Race Condition),导致数据不一致或程序行为异常。然而,一个常见的误解是,非线程安全的代码必然会立即表现出错误。事实上,在某些特定情况下,非线程安全的代码可能会意外地产生正确的结果,这往往给开发者带来困惑,并掩盖了潜在的并发问题。

竞态条件与非线程安全计数器示例

考虑一个简单的Java计数器类,它包含一个私有整数变量和一个递增该变量的方法:

public class Counter {
    private int counter = 0;

    public void incrementCounter() {
        counter += 1; // 这是一个复合操作:读取、修改、写入
    }

    public int getCounter() {
        return counter;
    }
}

这个Counter类是典型的非线程安全示例。incrementCounter()方法看起来是原子操作,但实际上它包含了三个独立的步骤:

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

  1. 读取counter的当前值。
  2. 将读取到的值加1。
  3. 将新值写回counter。

当多个线程同时调用incrementCounter()时,这些步骤的执行顺序可能会被打乱,导致某些递增操作丢失。例如,线程A读取counter为0,线程B也读取counter为0。线程A将其递增到1并写入,线程B也将其递增到1并写入。最终counter的值为1,而不是预期的2。

为了模拟这种竞态条件,我们通常会使用ExecutorService和CountDownLatch来协调多个线程的启动和结束:

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

public class Main {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        CountDownLatch startSignal = new CountDownLatch(10);
        CountDownLatch doneSignal = new CountDownLatch(10);
        Counter counter = new Counter(); // 非线程安全计数器实例

        for (int i = 0; i < 10; i++) {
            executorService.submit(() -> {
                try {
                    startSignal.countDown(); // 准备就绪
                    startSignal.await();     // 等待所有线程准备就绪
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt(); // 恢复中断状态
                    throw new RuntimeException(e);
                }

                counter.incrementCounter(); // 执行非线程安全递增
                doneSignal.countDown();     // 完成任务
            });
        }

        doneSignal.await(); // 等待所有任务完成
        System.out.println("Finished: " + counter.getCounter());
        executorService.shutdownNow(); // 关闭线程池
    }
}

这段代码创建了10个线程,每个线程都对同一个counter实例执行一次incrementCounter()。理论上,最终counter的值应该是10。然而,由于竞态条件,我们通常预期会看到一个小于10的值。但令人困惑的是,在某些运行环境下,上述代码可能每次都输出“Finished: 10”,这使得开发者误以为其代码是线程安全的。

免费语音克隆
免费语音克隆

这是一个提供免费语音克隆服务的平台,用户只需上传或录制一段 5 秒以上的清晰语音样本,平台即可生成与用户声音高度一致的 AI 语音克隆。

下载

理解“偶然正确性”:缺乏行为保证

为什么一个非线程安全的计数器有时会返回正确的值?这并非因为代码本身变得线程安全,而是因为非线程安全代码的本质是缺乏行为保证,而不是保证会失败。其背后的原因涉及多方面的复杂因素:

  1. JVM、JIT编译器与硬件的优化及调度不确定性:

    • JVM和JIT编译器的自由度: Java虚拟机(JVM)及其即时编译器(JIT)在运行时对代码进行优化时拥有很大的自由度。对于非同步的代码,它们可以进行指令重排、缓存优化等操作。在某些特定场景下,JIT编译器可能会将看似非原子的操作在特定硬件上优化成接近原子的行为,或者由于执行顺序的巧合,避免了竞态条件的发生。换句话说,优化器可能在不改变程序单线程语义的前提下,选择一种恰好能避免并发问题的实现方式,但这并非是其职责,也无任何保证。
    • 线程调度: 操作系统和JVM的线程调度器决定了线程的执行顺序和时间片分配。在某些运行中,10个线程的执行可能恰好是串行化的,或者它们的执行交错方式碰巧避免了读-改-写操作的冲突。例如,一个线程可能在另一个线程开始递增操作之前就完成了自己的递增。对于少量线程和少量操作,这种“幸运”的调度模式更容易出现。
    • 硬件内存模型: 不同的CPU架构有不同的内存模型。在某些弱内存模型下,CPU可能会对内存操作进行重排。然而,对于简单的int类型递增,在某些强一致性内存模型下,或者由于缓存行(cache line)的特性,短时间内的操作可能在CPU内部表现出一定的原子性,从而暂时掩盖了问题。
  2. Java内存模型(JMM)的影响: Java内存模型定义了线程如何以及何时可以看到其他线程写入的值,以及指令的执行顺序。对于非volatile或非synchronized的共享变量,JMM不保证一个线程对该变量的修改能立即被其他线程看到。同样,它也不保证指令的执行顺序。因此,即使代码在一次运行中“碰巧”正确,也可能在另一次运行中,由于内存可见性问题或指令重排,导致结果出错。这种不确定性是其非线程安全的核心体现。

  3. 竞态条件窗口的狭窄性: 在上述计数器示例中,counter += 1的复合操作虽然包含多个步骤,但其执行时间相对较短。当线程数量不多(例如10个)且每个线程只执行一次递增时,发生冲突的“窗口”非常小。这意味着,大多数时候,一个线程可能在另一个线程尝试访问counter之前,就已经完成了整个读-改-写周期。只有在非常精确的时机下,才能触发竞态条件,导致数据丢失

不可靠代码的危害

这种“偶然正确性”是并发编程中最危险的陷阱之一。它可能导致:

  • 难以复现的Bug: Bug只在特定硬件、特定JVM版本、特定负载或特定线程调度模式下出现,使得调试变得异常困难。
  • 生产环境灾难: 在开发和测试环境中看似正常的代码,一旦部署到生产环境,在更高的并发量和不同的运行条件下,可能立即崩溃或产生错误数据。
  • 虚假的安全感: 开发者可能会因为代码“看起来”正常而忽视了潜在的并发问题,导致后续的并发设计更加脆弱。

确保线程安全:可靠的解决方案

为了彻底消除这种不确定性,我们必须采用明确的同步机制来保证共享数据在多线程环境下的正确性。

  1. 使用synchronized关键字: 通过将incrementCounter方法声明为synchronized,可以确保同一时间只有一个线程能够执行该方法,从而避免竞态条件。

    public class SynchronizedCounter {
        private int counter = 0;
    
        public synchronized void incrementCounter() {
            counter += 1;
        }
    
        public synchronized int getCounter() {
            return counter;
        }
    }
  2. 使用java.util.concurrent.atomic包中的原子类: 对于简单的数值操作,Java提供了AtomicInteger、AtomicLong等原子类,它们内部使用了CAS(Compare-And-Swap)操作,可以在不使用锁的情况下保证操作的原子性,且通常比synchronized具有更好的性能。

    import java.util.concurrent.atomic.AtomicInteger;
    
    public class AtomicCounter {
        private AtomicInteger counter = new AtomicInteger(0);
    
        public void incrementCounter() {
            counter.incrementAndGet(); // 原子递增操作
        }
    
        public int getCounter() {
            return counter.get();
        }
    }

    使用AtomicCounter修改后的Main类如下:

    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class MainAtomic {
        public static void main(String[] args) throws InterruptedException {
            ExecutorService executorService = Executors.newFixedThreadPool(10);
    
            CountDownLatch startSignal = new CountDownLatch(10);
            CountDownLatch doneSignal = new CountDownLatch(10);
            AtomicCounter counter = new AtomicCounter(); // 使用线程安全计数器
    
            for (int i = 0; i < 10; i++) {
                executorService.submit(() -> {
                    try {
                        startSignal.countDown();
                        startSignal.await();
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        throw new RuntimeException(e);
                    }
    
                    counter.incrementCounter(); // 执行原子递增
                    doneSignal.countDown();
                });
            }
    
            doneSignal.await();
            System.out.println("Finished: " + counter.getCounter()); // 始终输出 10
            executorService.shutdownNow();
        }
    }

    运行MainAtomic,无论在何种环境下,都将稳定地输出“Finished: 10”。

总结:优先保证,而非依赖巧合

非线程安全代码有时能产生正确结果的现象,是并发编程中一个重要的学习点。它提醒我们:线程安全的核心在于提供行为保证,而不是仅仅观察到正确的结果。 程序的正确性不应依赖于JVM、JIT编译器或操作系统调度器的偶然行为。作为专业的开发者,我们必须始终遵循并发编程的最佳实践,使用synchronized、volatile、java.util.concurrent.atomic包中的原子类、锁(Lock接口)或并发集合等工具,明确地处理共享数据的访问,从而构建健壮、可预测且可靠的多线程应用。任何时候,当涉及到共享的可变状态时,都应该假定它可能在没有同步的情况下出错,并主动采取措施来防止这种情况的发生。

相关文章

编程速学教程(入门课程)
编程速学教程(入门课程)

编程怎么学习?编程怎么入门?编程在哪学?编程怎么学才快?不用担心,这里为大家提供了编程速学教程(入门课程),有需要的小伙伴保存下载就能学习啦!

下载

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
DeepSeek
DeepSeek

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

豆包大模型
豆包大模型

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

通义千问
通义千问

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

腾讯元宝
腾讯元宝

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

文心一言
文心一言

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

讯飞写作
讯飞写作

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

即梦AI
即梦AI

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

ChatGPT
ChatGPT

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

相关专题

更多
string转int
string转int

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

1010

2023.08.02

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

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

611

2024.08.29

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

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

334

2025.08.29

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

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

235

2025.08.29

c++中volatile关键字的作用
c++中volatile关键字的作用

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

75

2025.10.23

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

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

1923

2023.10.19

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

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

656

2025.10.17

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

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

2392

2025.12.29

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

3

2026.03.11

热门下载

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

精品课程

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

共23课时 | 4.3万人学习

C# 教程
C# 教程

共94课时 | 11.1万人学习

Java 教程
Java 教程

共578课时 | 80.6万人学习

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

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