Java并发编程实战 10 | 线程安全问题

news2024/11/25 12:57:20

什么是线程安全?

《Java并发实践》的作者 Brian Goetz 对线程安全的定义是:当多个线程访问同一个对象时,如果无需考虑这些线程在运行时的调度策略和交替执行顺序,也不需要进行额外的同步处理,仍然能够得到正确的结果,那么这个对象就是线程安全的。

简单来说,线程安全意味着:无论有多少线程同时访问业务中的某个对象或方法,都不需要做额外的处理(就像编写单线程程序一样),程序依然能够正常运行,不会因多线程的并发访问而出现错误。

什么是线程不安全?

当多个线程同时访问一个对象时,如果一个线程正在更新该对象的值,而另一个线程也在同时修改或读取这个对象的值,就有可能导致得到不正确的数据,这就是线程不安全。

这种情况下,为了确保结果的正确性,我们需要采取额外的措施,比如使用 synchronized 关键字对这部分代码进行同步,以确保同一时间只有一个线程能够访问或修改该对象。

为什么不是所有的程序都设计成线程安全的?

这主要出于程序性能、设计复杂度成本等考虑。

  • 性能开销: 设计和实现线程安全的代码通常需要额外的同步机制,如锁(synchronized)、显式锁(ReentrantLock)、或其他并发工具。这些同步机制虽然能确保线程安全,但也会引入性能开销,例如线程上下文切换、锁竞争和死锁的风险等。因此,在一些对性能要求极高的场景下,可能会选择避免过度同步,以提高程序的效率。

  • 复杂性增加: 使程序线程安全通常会增加代码的复杂性。线程安全的设计需要考虑多个线程之间的交互,避免竞争条件、死锁等问题,这会增加开发和维护的难度。在一些情况下,开发人员可能会选择简化设计,而不是一开始就实现线程安全的解决方案。

线程安全问题的分类

运行结果不正确

当多个线程同时操作一个共享变量时,可能会导致运行结果出现意料之外的错误。比如,假设我们有两个线程,每个线程都对一个count变量进行10000次递增操作。

public class ResultError {

    static int count;

    public static void main(String[] args) throws InterruptedException {

        Runnable runnable = () -> {
            for (int i = 0; i < 10000; i++) {
                count++;
            }
        };

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);  
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(count);
    }
}
//输出情况1:
14088

理论上,最终count的值应该是20000(10000 + 10000),但实际输出的结果往往小于20000,而且每次运行的结果可能都不一样。这是为什么呢?

这是因为在多线程场景下,CPU 的调度是以时间片为单位进行分配的,每个线程可以获得一定量的时间片。但是如果线程所拥有的时间片耗尽,就会被挂起,将 CPU 资源让给其他线程,这样可能会产生线程安全问题。

例如,虽然 count++ 操作看起来是一行代码,但它实际上并不是一个原子操作。count++ 操作分为三个主要步骤:

  • 读取:读取 count 变量的当前值。
  • 增加:将读取的值加 1。
  • 保存:将增加后的值写回 count 变量。

那么让我们看看线程不安全是如何发生的。

让我们按顺序分析一下这个过程:

  1. 线程1 先读取 count 的值,假设当前 count = 1。

  2. 接着,线程1 计算 count + 1,但还没有将结果保存回 count。

  3. 此时,线程1 被挂起,CPU 开始执行 线程2。线程2 也读取了 count 的值,这时 count 仍然是 1,因为线程1 的更新还没有保存到 count 中。

  4. 线程2 进行和线程1 相同的操作:计算 count + 1。由于线程2 读取到的 count 仍然是 1,它也将计算结果加 1。

  5. 此时,线程2 被挂起,CPU执行线程1 ,线程1 将其计算结果(即 count + 1=2)写回 count,然后线程1 结束,线程2 重新被调度执行。

  6. 线程2 继续执行,将其计算结果(即 count + 1=2)写回 count,此时线程1 和线程2 都进行了相同的操作,覆盖了其中一次 count 的更新。

由于线程1 的更新结果在线程2 执行之前并没有被保存,这导致线程2 和线程1 都基于相同的旧值 count = 1 进行操作,最终的结果只反映了最后一个线程的更新。因此,count 的实际值小于理论值,这就是线程安全问题的根源。

如何解决?

为了解决这个问题,我们可能需要这样的解决方案:当有多个线程对共享变量进行操作时,需要保证同一时刻只有一个线程在操作这个变量,当一个线程正在执行的时候其他线程必须等待,直到当前线程处理完数据。

这种解决方案的一种实现方式是互斥锁(mutex),互斥锁是一种可以确保互斥访问的工具。也就是说,当一个线程获取了锁并访问共享数据时,其他线程必须等待,直到这个线程释放锁。

在Java中,我们可以通过 synchronized 关键字来实现这一点。synchronized 可以修饰方法或代码块,以保证在同一时刻只有一个线程能够执行被修饰的代码段。这样可以确保线程安全,避免多个线程同时操作共享变量时发生冲突。

示例代码如下:

public class ResultErrorResolution {

    static int count;

    public static void main(String[] args) throws InterruptedException {

        Runnable runnable = () -> {
            synchronized (ResultErrorResolution.class) {
                for (int i = 0; i < 10000; i++) {
                    count++;
                }
            }
        };

        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(count);
    }
}
//输出:
20000

输出结果已经符合我们的预期。

关于synchronized关键字,我们将在后面的文章中详细介绍。目前你只需要了解它可以确保同一时刻最多只有一个线程可以执行这段代码(需要持有对应的锁,本例中为ResultErrorResolution.class),从而实现并发安全控制。

线程活跃性问题

第二类线程安全问题统称为活跃性问题。那么,什么是活跃性问题呢?

活跃性问题是指程序在运行时永远无法达到预期的最终结果。

活跃性问题最常见的三种类型是:死锁、活锁和饥饿。由于这些内容涉及的细节较多,后面会有单独的文章介绍这个主题。

这类问题的后果通常比其他类型的错误更加严重。例如,死锁可能导致程序完全停滞,无法继续执行。

对象发布和初始化期间的安全问题

最后,我们来讨论对象发布和初始化过程中可能引发的线程安全问题。创建对象并将其发布和初始化,以供其他类或对象使用,是一种常见的操作。然而,如果在不恰当的时机或错误的位置执行这些操作,就可能导致线程安全问题。

让我们看一个例子:

public class InitError {

    private Map<Long, String> students;

    public InitError() {
        new Thread(() -> {
            students = new HashMap<>();
            students.put(1L, "Tom");
            students.put(2L, "Bob");
            students.put(3L, "Victor");
        }).start();
    }

    public Map<Long, String> getStudents() {
        return students;
    }

    public static void main(String[] args) throws InterruptedException {
        InitError initError = new InitError();
        System.out.println(initError.getStudents().get(1L));
    }
}

在这个例子中,我们定义了一个Map类型的成员变量students,其中Map的key是学号,value是学生姓名。在构造函数中,我们启动了一个新的线程,并在该线程中将学生信息赋给students变量。

然而,问题在于新启动的线程只有在执行完run()方法中的所有操作后,students才算完全初始化完成。但在main函数中,初始化完InitError类之后,程序并没有等待线程完成,而是直接尝试获取1号学生的信息。

想象一下此时程序会发生什么情况?我们来看看结果:

可以看出程序会发生空指针异常。为什么会出现这种情况呢?

这是因为students成员变量是在构造函数中通过一个新建的线程进行初始化和赋值的,而线程的启动和执行需要一定的时间。如果main函数不等待线程完成初始化,直接尝试获取数据,就很可能会遇到getStudents方法返回null的情况。

当然,也有运行成功的可能,但概率不大,取决于线程调度的结果。

因此,确保对象在发布和初始化过程中的线程安全非常重要,不正确的操作可能导致其他线程在对象尚未完全初始化时就访问它,从而引发各种错误和不稳定性。

5.哪些场景需要特别注意线程安全问题?

1. 访问共享变量或资源

第一种场景是访问共享变量或共享资源,例如访问静态变量、共享缓存等。这些资源通常会被多个线程同时访问,从而可能导致线程安全问题。例如,我们之前提到的count++操作就是一个典型的案例。

另外,在对共享变量进行“检查并执行”操作时,也可能出现问题。由于这种操作不是原子的,可能会在执行过程中被中断,而在恢复执行时,之前检查的结果可能已经失效或过期。这样就会导致不一致的状态或错误的行为。

例如:

if (count == 10) {
    count = count * 10;
}

可能会有多个线程同时满足条件count == 10,因此会多次执行count = count * 10,导致结果不正确。在这种情况下,我们需要使用加锁等保护措施,以避免出现线程安全问题。

2. 不同数据之间存在绑定关系

第二种场景是当不同的数据之间存在关联时。某些情况下,不同的数据是成组出现的,它们相互对应或绑定,最典型的例子就是 IP 和端口号。例如,当我们更改 IP 时,通常需要同时更改端口号。如果这两个操作没有绑定在一起,就可能会出现只修改了 IP 或端口号之一的情况。

在这种情况下,如果信息已经被发布,接收方可能会获得一个错误的 IP 和端口号组合,导致线程安全问题。因此,为了避免这种情况,我们需要确保这些操作是原子的,即要么全部成功执行,要么全部失败。

3. 所依赖的类没有声明自身是线程安全的。

第三种场景是在使用其他类时,如果该类没有声明自己是线程安全的,那么在对其进行并发的多线程操作时,可能会引发线程安全问题。例如,ArrayList本身并不是线程安全的,如果多个线程同时访问和操作一个ArrayList实例,就可能导致数据错误或不一致。

需要注意的是,这种情况下问题的责任并不在于ArrayList,因为它本身并不保证线程安全,正如其源代码注释中所描述的那样。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2118091.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C语言--12字符串处理函数

函数strstr 函数strchr与strrchr 注意&#xff1a; 这两个函数的功能&#xff0c;都是在指定的字符串 s 中&#xff0c;试图找到字符 c。strchr() 从左往右找第一个&#xff0c;strrchr() 从左往右找最后一个。字符串结束标记 ‘\0’ 被认为是字符串的一部分。 函数strlen 示例…

一款免费开源功能丰富的看图软件NeeView

NeeView 是一款功能丰富的图像查看软件&#xff0c;它以其独特的浏览体验和广泛的支持格式受到用户的欢迎。NeeView 不仅可以浏览普通的图像文件&#xff0c;还能够查看压缩包内的图片、预览PDF文档甚至播放视频文件。 NeeView 的主要特点&#xff1a; 多格式支持&#xff1a…

《人工智能安全治理框架》1.0版

人工智能是人类发展新领域&#xff0c;给世界带来巨大机遇&#xff0c;也带来各类风险挑战落实《全球人工智能治理倡议》&#xff0c;遵循“以人为本、智能向善”的发展方向&#xff0c;为推动政府、国际组织、企业、科研院所、民间机构和社会公众等各方&#xff0c;就人工智能…

无人机之穿越机的类型

穿越机&#xff0c;即FPV Drone或Racing Drone&#xff0c;是一种主要通过第一人称视角&#xff08;FPV&#xff09;进行操作的无人机。这种无人机通常配备有四个电机和相应的飞控系统&#xff0c;使其具有极高的飞行自由度和速度。穿越机的类型多样&#xff0c;可以从不同角度…

GD32E230程序烧录和开发环境使用介绍

GD32E230程序烧录和开发环境使用介绍 从GD32提供的资料来看&#xff0c;支持IAR、Keil、EmbeddedBuilder&#xff1b;目前该软件还是比较粗糙&#xff0c;个人上手体验不佳&#xff0c;面板菜单按键烧操作一下&#xff0c;动不动就卡死&#xff0c;仅支持gdlink调试器。 Embed…

第100+24步 ChatGPT学习:概率校准 Beta Calibration

基于Python 3.9版本演示 一、写在前面 最近看了一篇在Lancet子刊《eClinicalMedicine》上发表的机器学习分类的文章&#xff1a;《Development of a novel dementia risk prediction model in the general population: A large, longitudinal, population-based machine-learn…

元宇宙的崛起:重塑2024年游戏行业的新趋势

最早可以追溯到1994年&#xff0c;当时出现了世界上第一个轴测图界面的多人互动社交游戏《Web World》。‌这个游戏允许用户实时聊天、旅游、改造场景&#xff0c;开启了游戏中的UGC模式&#xff0c;可以视为元宇宙游戏的雏形。 2021年Roblox元宇宙的概念股上市&#xff0c;Fac…

学生护眼台灯哪个品牌比较好?分享五款效果好的学生护眼台灯

现在孩子的很多兴趣班和课后辅导班都是在线上举行&#xff0c;通常对着手机电脑长时间。电子产品有大量蓝光和辐射&#xff0c;会伤害到孩子的眼睛。但为了学习&#xff0c;也是没办法。护眼台灯的出现可以让孩子们的眼睛得到保护&#xff0c;防止蓝光对眼睛的伤害。学生护眼台…

用命令行的方式启动.netcore webapi

用命令行的方式启动.netcore web项目 进入指定的项目文件夹&#xff0c;比如我发布后的代码放在下面文件夹中 在此地址栏中输入“cmd”&#xff0c;打开命令提示符&#xff0c;进入到发布代码目录 命令行启动.netcore项目的命令为: dotnet 项目启动文件.dll --urls"ht…

CSP-J基础之数学基础 计数原理与排列组合 一篇搞懂

文章目录 前言加法原理加法原理是什么使用场景 乘法原理举个例子总结 区别加法原理乘法原理总结 乘法原理的运用排列组合**排列****组合**总结 计算排列的可能种数举个例子数学定义数学公式应用公式例子应用 总结全排列举个例子数学定义数学公式作用 组合数学定义组合的公式举个…

系统安全设计规范(Word完整版)

1.1 总体设计 1.1.1 设计原则 1.2 物理层安全 1.2.1 机房建设安全 1.2.2 电气安全特性 1.2.3 设备安全 1.2.4 介质安全措施 1.3 网络层安全 1.3.1 网络结构安全 1.3.2 划分子网络 1.3.3 异常流量管理 1.3.4 网络安全审计 1.3.5 网络访问控制 1.3.6 完整性检查 1.…

天洑软件荣获国家级专精特新“小巨人”企业认定

近日&#xff0c;江苏省工业和信息化厅公布了第六批国家级专精特新"小巨人"企业名单&#xff0c;南京天洑软件有限公司&#xff08;以下简称“天洑软件”&#xff09;获得国家级专精特新“小巨人”企业认定。继2023年被评为江苏省“专精特新”中小企业后&#xff0c;…

【828华为云征文|华为云Flexus X实例:从选购到登录,一站式指南】

华为云Flexus X实例&#xff1a;从选购到登录&#xff0c;一站式指南 华为云Flexus X实例的优势大揭秘操作指南&#xff1a;一步步带你开通华为云Flexus X实例注册与登录华为云账号选择配置并购买选择Flexus X实例配置选择基础配置实例规格镜像存储网络弹性公网IP您可能需要&am…

佰朔资本:换手率是什么指标?换手率高股价为什么不涨呢?

换手率&#xff0c;也叫”周转率“&#xff0c;指的是在必定时间内商场中股票易手买卖的频率&#xff0c;是反映股票流通性强弱的指标之一。 换手率某段时期内成交量/发行总股数*100%。 通常而言&#xff0c;在股票商场上&#xff0c;换手次数多&#xff0c;筹码互动多&#…

Docker 部署 Kafka (图文并茂超详细)

部署 Kafka ( Docker ) Kafka对于zookeeper是强依赖&#xff0c;保存kafka相关的节点数据&#xff0c;所以安装Kafka之前必须先安装zookeeper [Step 1] : 部署 Zookeeper -> 拉取 Zookeeper 镜像 ➡️ 启动 Zookeeper 容器 docker pull zookeeper:3.4.14 docker run -d --…

Linux网络:网络协议栈协议

1.网络在体系结构的位置与网络协议栈的层状结构 2.协议栈各层的功能 协议栈分层设计达到了解耦目的&#xff0c;层与层之间只有接口之间的关系&#xff0c;提高了代码之间的可维护性与拓展性。同一层之间使用的协议相同&#xff0c;达到了跨设备的作用 3.协议 协议本质是一…

【详解】文件操作,Stream流

文件(File)操作——I/O流 Windows&#xff08;大多数&#xff09;进行文件操作的类File。 文件?文件夹?路径? 文件 能够使用工具打开操作的&#xff0c;文件是不能存储文件的。 一般文件具有后缀——.mp4 文件夹 存储文件的 路径问题——“/” 正右\ 反左/ ——统一朝左&am…

什么是点对点专线、SDH专线以及MSTP专线?

点对点专线&#xff08;Point-to-Point Circuit&#xff09;、SDH专线&#xff08;Synchronous Digital Hierarchy&#xff09;以及MSTP专线&#xff08;Multi-Service Transport Platform&#xff09;都是企业级通信服务中常见的网络连接类型&#xff0c;主要用于提供高带宽、…

SprinBoot+Vue停车场管理系统的设计与实现

目录 1 项目介绍2 项目截图3 核心代码3.1 Controller3.2 Service3.3 Dao3.4 application.yml3.5 SpringbootApplication3.5 Vue 4 数据库表设计5 文档参考6 计算机毕设选题推荐7 源码获取 1 项目介绍 博主个人介绍&#xff1a;CSDN认证博客专家&#xff0c;CSDN平台Java领域优质…

《语文新读写》是知网收录吗?语文新读写编辑部查询

《语文新读写》是知网收录吗&#xff1f;语文新读写编辑部查询 《语文新读写》是知网收录。 一、期刊简介 《语文新读写》是经国家新闻出版总署正式批准&#xff0c;由上海世纪出版&#xff08;集团&#xff09;有限公司主管&#xff0c;上海少年儿童出版社有限公司主办的综合…