并发编程陷阱:32位CPU下long写操作的线程安全漏洞

1. 现象描述

1.1 Bug问题简述

在多线程环境下操作共享数据时,往往面临各种并发问题。其中,一种常见的情况是,即使一段代码在单线程下执行没有问题,当它在多线程环境下执行时,却可能由于线程安全问题导致意想不到的Bug。对于使用32位操作系统的多核CPU,当多个线程尝试同步写入long型变量时,有时候会出现一个线程写入的值与另一个线程读取到的值出现不一致的问题。

1.2 多线程环境下的long型变量不一致示例

public class LongVisibilityTest extends Thread {private static long field = 0;private volatile boolean done = false;private final long value;public LongVisibilityTest(long value) {this.value = value;}@Overridepublic void run() {while (!done) {field = value;}}public static void main(String[] args) throws InterruptedException {LongVisibilityTest t1 = new LongVisibilityTest(0xFFFFFFFF);LongVisibilityTest t2 = new LongVisibilityTest(0x00000000);t1.start(); t2.start();Thread.sleep(100);t1.done = true; t2.done = true;t1.join(); t2.join();System.out.println("Field value: " + Long.toHexString(field));}
}

在上面的代码中,两个线程尝试写入不同的long值,按理来说因为done是volatile的,一旦变量done被设置为true,线程应当停止,并且field的值应当是最后被写入的值。但在32位操作系统的多核机器上,有可能会发现field的值是两个值的“混合体”,这正是要解释的Bug所在。

2. 背后原理解析

2.1 数据类型与内存模型

要理解在多线程环境下操作变量时可能遇到的问题,首先需要了解JVM中的数据类型和内存模型。在Java中,数据类型分为原始数据类型和引用数据类型。对于原始数据类型,例如long和double,它们在32位JVM上不是原子性操作,因为它们占用64位,超过了32位系统的原子性操作保证范围。
内存模型定义了共享内存中变量的读写方式,Java内存模型(JMM)规定了线程和主内存之间的交互行为。在JMM中,线程对变量的所有操作都必须在工作内存中进行,而不能直接对主内存进行操作,并且每个线程不能直接访问其他线程的工作内存。

2.2 CPU架构对变量读写的影响

32位CPU,指的是这样的处理器架构,它的寄存器宽度、数据总线宽度和地址总线宽度都是32位。因此,该架构下的CPU一次性能处理的最大数据宽度是32位。因此,对于64位宽的数据(如Java中的long或double类型),CPU需要分成两次操作来读写,这就意味着在多线程并发的环境中,当两个线程同时对一个64位的long型变量进行操作时,可能会导致数据的不一致。

2.3 Java内存模型(JMM)对并发编程的意义

Java内存模型是Java并发编程的基石,它抽象了内存交互的细节,简化了程序员对同步的处理。JMM解决了原子性、可见性和有序性这三个关键问题,特别是在多核处理器上编程时这些问题尤其重要。原子性保证了指令的不可分割性,可见性确保了一个线程对共享变量值的修改,能够及时地被其他线程看到,有序性则是关于指令执行顺序的问题。

3. 32位CPU与long型变量写操作的问题

3.1 什么是原子性操作?

原子性操作指的是不可被线程调度机制打断的操作,它一旦开始,就一直运行到结束,中间不会有上下文切换。在Java中,原子性操作通常是指一个或多个操作在CPU执行的过程中不能被中断的操作序列。这对于同步非常关键,因为原子性的缺失将导致线程安全问题。

3.2 32位单核CPU对long型变量的处理

在32位单核CPU系统中,尽管64位的数据类型需要分两步来保证操作的完整性,但由于同一时刻只有一个操作,所以不存在其他线程干扰的情况,原子性得以保证。

3.3 32位多核CPU对long型变量的处理

然而,在32位多核CPU系统中,操作系统会在多个核心之间调度线程。如果两个线程在不同的核心上运行,并尝试修改同一个long型变量,则可能出现其中一个线程的修改只完成了一半(32位),而此刻另一线程开始执行操作,就会导致所谓的“诡异Bug”,即最终读取的值可能是两次操作中的一部分。

3.4 指令重排和内存屏障

在计算机执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排。这可能会导致,在多线程环境中,读写操作的执行顺序与代码中的顺序不一致。为了解决指令重排带来的问题,可以用内存屏障(Memory Barriers),它是一种使得特定操作顺序固定的机制,并保证特定的内存读写操作执行的可见性。
下面是一个简单的示例,揭示了volatile的关键字能够插入内存屏障来防止指令重排,同时也能保证操作的可见性。

public class VolatileExample {private static volatile long counter = 0;public static void main(String[] args) throws InterruptedException {Thread writer = new Thread(() -> {counter = 0xAAAA_BBBB_CCCC_DDDD;});Thread reader = new Thread(() -> {long value = counter; // 此处的读操作将看见writer线程写入的最新值System.out.println("Read value: " + Long.toHexString(value));});writer.start();writer.join();reader.start();reader.join();}
}

在上面的代码中,我们使用volatile来声明counter变量。这个关键字确保了在多核环境下,每次写入时都将counter的新值同步回主内存,在读取时都从主内存中读取最新的值,从而避免了缓存不一致和指令重排造成的问题。

4. 实战案例详解

4.1 制造问题现象的测试案例

为了更直观地理解在32位多核CPU上执行long型变量写操作可能出现的诡异Bug问题,我们可以构建一个简单的Java测试程序。这个程序会启动两个线程,一个线程将long型变量写为全1,另一个将其写为全0。按照预期,我们认为变量的值最终要么是全1,要么是全0。

public class LongTest {private static long testValue = 0L;public static void main(String[] args) {Runnable run1 = () -> testValue = 0xFFFFFFFFFFFFFFFFL; // 所有位都是1Runnable run2 = () -> testValue = 0x0L; // 所有位都是0Thread t1 = new Thread(run1);Thread t2 = new Thread(run2);t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {Thread.currentThread().interrupt();}System.out.printf("Test Value: 0x%016X%n", testValue);}
}

执行上述测试程序,在32位多核处理器的机器上,可能会观察到诡异Bug问题——最终输出的testValue并不总是全1或全0,有时候是两者的混合值。

4.2 使用Java代码揭示Bug产生的条件

上述现象的出现条件是,在32位操作系统下,当一个线程在读写64位的long型变量时,CPU将这个操作分成两步32位的读写来执行。如果在这个过程中,另一个线程介入,进行了写操作,就会导致数据的不一致。
例如,线程A写入0xFFFFFFFFFFFFFFFFL,在执行这个操作的过程中(假设已经完成了前32位的写入),线程B开始写入0x0L,线程B可能只完成了写入的一半就被线程调度器暂停了,这时CPU的两次32位写入操作就会交织在一起,最终导致了错误的结果。

4.3 线程安全与数据竞争的分析

数据竞争发生在多个线程在无同步的情况下访问同一资源,并且至少有一个线程写入资源的场合。在我们的案例中,两个线程在试图修改同一long型变量而没有进行同步,就发生了数据竞争,导致了线程安全问题。
为了解决这个问题,我们必须确保对long型变量的写操作是原子的,也就是说,在写操作完成之前,不允许其他线程对这个变量进行读或写操作。这可以通过Java中的同步机制来实现,例如synchronized关键字,或java.util.concurrent.atomic包中的原子变量类。

5. 可行的解决方案

5.1 volatile关键字和它的作用

volatile是Java提供的一种轻量级的同步机制,它确保了变量的可见性和禁止指令重排序。使用volatile声明的变量,可以确保任何一个线程在读取该变量时都能得到最近一次被另一个线程写入的值。

public class VolatileLongTest {private static volatile long testValue = 0L;public static void main(String[] args) {Runnable run1 = () -> testValue = 0xFFFFFFFFFFFFFFFFL; // 所有位都是1Runnable run2 = () -> testValue = 0x0L; // 所有位都是0Thread t1 = new Thread(run1);Thread t2 = new Thread(run2);t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {Thread.currentThread().interrupt();}System.out.printf("Volatile Test Value: 0x%016X%n", testValue);}
}

使用volatile之后,即便在多核心环境下,我们也能保证在任何时候,testValue都存储着最新的、由某一个线程写入的值。

5.2 synchronized同步锁的使用

synchronized关键字可以用来控制对共享资源的并发访问。它可以确保同一时刻只有一个线程可以执行某段代码,从而避免并发问题。

public class SynchronizedLongTest {private static long testValue = 0L;public synchronized static void setTestValue(long newValue) {testValue = newValue;}public static void main(String[] args) {Runnable run1 = () -> setTestValue(0xFFFFFFFFFFFFFFFFL);Runnable run2 = () -> setTestValue(0x0L);Thread t1 = new Thread(run1);Thread t2 = new Thread(run2);t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {Thread.currentThread().interrupt();}System.out.printf("Synchronized Test Value: 0x%016X%n", testValue);}
}

在这个例子中,我们使用synchronized关键字保护对testValue的访问,确保每次只有一个线程能够修改它。

5.3 原子类的使用

Java语言中提供了一套原子变量类,比如AtomicLong,可以用来保证64位的长整型数的原子性操作。

import java.util.concurrent.atomic.AtomicLong;public class AtomicLongTest {private static AtomicLong testValue = new AtomicLong(0L);public static void main(String[] args) {Runnable run1 = () -> testValue.set(0xFFFFFFFFFFFFFFFFL);Runnable run2 = () -> testValue.set(0x0L);Thread t1 = new Thread(run1);Thread t2 = new Thread(run2);t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {Thread.currentThread().interrupt();}System.out.printf("Atomic Test Value: 0x%016X%n", testValue.get());}
}

使用AtomicLong之后,即使在多线程环境下,写入和读取操作都是原子性的,避免了竞态条件的发生。

5.4 最佳实践和性能考量

在解决多线程环境下的并发问题时,我们需要衡量同步机制的性能影响。volatile虽然解决了可见性问题,但不适合在大量写操作的场合使用。而synchronized可以解决多线程的同步问题,但可能会引入锁竞争,降低系统性能。原子类提供了无锁的线程安全操作,通常比synchronized更高效,但是其使用也是有开销的,适合于高并发、低竞争的环境。
因此,应用开发人员需要根据实际的应用场景、数据的读写模式,以及性能需求,来选择合适的同步策略。

6. 避免此类Bug的编码技巧

6.1 编码规范和最佳实践

在并发编程中遵循一定的编码规范和最佳实践可以极大减少并发Bug的发生。主要包括:

  • 尽量使用局部变量而非共享变量。
  • 共享变量应尽量声明为final或不可变。
  • 明确并限制共享变量的访问范围,可以通过使用封装的方法来操作共享数据。

6.2 多线程编程注意事项

在多线程编程时应当特别注意以下几点:

  • 明确区分变量是由哪些线程共享。仅在必要的地方使用线程同步机制。
  • 在设计阶段就考虑线程安全,利用Java提供的concurrent包下的工具类和接口。
  • 尽可能使用无锁编程方法,比如使用CopyOnWriteArrayList等线程安全的集合类。

6.3 实用工具和库的推荐

使用成熟的并发工具和库可以极大提升开发效率和程序运行时的稳定性,有以下推荐:

  • java.util.concurrent包含了多种并发工具类,如ExecutorService、CountDownLatch等。
  • Guava库提供了许多有用的并发工具类。
  • 使用Lombok注解库提供的@Synchronized注解来简化同步代码的编写。
import java.util.concurrent.locks.ReentrantLock;public class AvoidBugWithReentrantLock {private static final ReentrantLock lock = new ReentrantLock();private static long testValue = 0L;public static void setTestValue(long newValue) {lock.lock();try {testValue = newValue;} finally {lock.unlock();}}public static void main(String[] args) {Runnable run1 = () -> setTestValue(0xFFFFFFFFFFFFFFFFL);Runnable run2 = () -> setTestValue(0x0L);Thread t1 = new Thread(run1);Thread t2 = new Thread(run2);t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {Thread.currentThread().interrupt();}System.out.printf("Test Value after lock: 0x%016X%n", testValue);}
}

在这个示例中,我们使用了ReentrantLock来保证设置testValue的原子性操作。

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

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

相关文章

ADS过孔---过孔建模自动化

当前快速建模的方法有两类:一是脚本自动化,也就是今天要分享的方法,但该方法需要工程师有基本的脚本编辑能力,然后根据自己的需要去修改,难度较大一点点;二是参数化建模,也就是在GUI界面输入相应…

百度语音识别开发笔记

目录 简述 开发环境 1、按照官方文档步骤开通短语音识别-普通话 2、创建应用 3、下载SDK 4、SDK集成 5、相关接口简单说明 5.1权限和key 5.2初始化 5.3注册回调消息 5.4开始转换 5.5停止转换 6、问题 简述 最近想做一些语音识别的应用,对比了几个大厂…

华为手机连接电脑后电脑无反应、检测不到设备的解决方法

本文介绍华为手机与任意品牌电脑连接时,出现连接后电脑无反应、检测不到手机连接情况的解决方法。 最近,因为手机的存储空间愈发紧缺,所以希望在非华为电脑中,将华为手机内的照片、视频等大文件备份、整理一下。因此,需…

aardio爬虫) 实战篇:逆向有道翻译web接口

前言 之前的文章把js引擎(aardio封装库) 微软开源的js引擎(ChakraCore))写好了,这篇文章整点js代码来测一下bug。测试网站:https://fanyi.youdao.com/index.html#/ 逆向思路 逆向思路可以看有道翻译js逆向(MD5加密,AES加密&…

cmake进阶:定义函数的内部变量

一. 简介 前一篇文章学习 cmake中的定义函数基本用法。文章如下: cmake进阶:定义函数的使用方法-CSDN博客 本文继续学习 cmake中的定义函数,主要学习函数的内部变量。 二. cmake进阶:定义函数的内部变量 上一篇文章说过&…

Elasticsearch:理解人工智能相似性搜索

理解相似性搜索(也称为语义搜索)的指南,这是人工智能最新阶段的关键发现之一。 最新阶段人工智能的关键发现之一是根据相似性搜索和查找文档的能力。相似性搜索是一种比较信息的方法,其基于含义而非关键字。 相似性搜索也被称为语…

Stable Diffusion学习记录

文章目录 前言电脑配置推荐环境搭建下载地址安装步骤步骤一,打开下载的秋叶整合包,路径秋叶整合包/sd-wenui-aki步骤二,打开下载好的sd-webui-aki-v4.8.7解压包 Stable Diffusion软件配置,插件安装,模型下载Stable Dif…

从ETL与ELT谈起,理解数仓的任务

最近有个朋友,有几十 PB 的异构数据,数据源包括 MySQL、DB2、Oracle、CSV、磁带机,等等,然后他需要把这些数据中的一些信息做关联整合,从这几十 PB 的数据中提取出若干业务字段到数据仓库,做统一分析。 数…

Codeforces Round 943 (Div. 3) C-G

C. Assembly via Remainders 思路: 我们可以注意到,数组的长度只有 500 500 500 ,并且每个数的大小都在 500 500 500 以内,再看向这题,容易知道,当第一个数确定之后,之后所有的数字都会确定下…

uniapp自定义websocket类实现socket通信、心跳检测、连接检测、重连机制

uniapp自定义websocket类实现socket通信、心跳检测、检测连接、重连机制,仿vue-socket插件功能实现发送序列号进行连接检测,发送消息时42【key,value】格式,根据后端返回数据和需要接收到的数据做nsend与onSocketMessage的修改 import {publ…

leetcode-没有重复项的全排列-97

题目要求 思路 1.递归,如果num和n的元素个数一样就可以插入res中了,这个作为递归的结束条件 2.因为这个题是属于排列,并非组合,两者的区别是排列需要把之前插入的元素在回退会去,而组合不需要,因此会存在一…

14【PS作图】像素画尺寸大小

【背景介绍】本节介绍像素图多大合适 下图是160*144像素大小,有一个显示文本的显示器,还有一个有十几个键的键盘 像素画布尺寸 电脑16像素,但还有一个显示屏 下图为240*160 在场景素材,和对话素材中,用的是不同尺寸的头像,对话素材中的头像会更清楚,尺寸会更大 远处…

【软考高项】三十三、质量管理

一、管理基础 质量定义 国际标准:反映实体满足主体明确和隐含需求的能力的特性总和。 国家标准:一组固有特性满足要求的程度。固有特性是指在某事或某物中本来就有的,尤其是那种永久的可区分的特征。 ➢ 对产品来说,例如…

Flask 路由基础和封装

Flask 路由 Flask中的路由是用来定义应用程序中的 URL 和处理函数之间的映射关系的,而URL则是用户访问应用程序的入口点。通过路由,我们可以将用户访问的 URL 映射到对应的视图函数上,从而实现不同的功能。 一、路由基础 1.定义路由: 我们可以使用 app.route() …

查看微信小程序主包大小

前言 略 查看微信小程序主包大小 在微信开发者工具右上角找到“详情->基本信息” 查看微信小程序主包构成 通过微信开发者工具中的“代码依赖分析”工具查看

置身事内 书摘

信息的重要性:所谓山高皇帝远,上级领导不可能掌握和处理所有信息,故常常不得不依赖下级提供的信息,内容是否可靠,上级不见得知道,因此可能被下级牵着鼻子走。但因为信息复杂,不易处理&#xff0…

Unity 性能优化之光照优化(七)

提示:仅供参考,有误之处,麻烦大佬指出,不胜感激! 文章目录 前言一、测试目的一、实时光源是什么?二、开始测试1.场景中只有一个光照的数值情况2.添加4个点光源后4.结果 总结 前言 实时光源数量越多&#x…

分享一个国内可用的AIGC网站,免费无限制,支持AI绘画

背景 AIGC作为一种基于人工智能技术的自然语言处理工具,近期的热度直接沸腾🌋。 作为一个AI爱好者,翻遍了各大基于AIGC的网站,终于找到一个免费!免登陆!手机电脑通用!国内可直接对话的AIGC&am…

保持亮灯:监控工具如何确保 DevOps 中的高可用性

在快速发展的 DevOps 领域,保持高可用性 (HA) 至关重要。消费者期望应用程序具有全天候响应能力和可访问性。销售损失、客户愤怒和声誉受损都是停机的后果。为了使 DevOps 团队能够在问题升级为中断之前主动检测、排除故障并解决问题,监控工具成为这种情…

nginx--tcp负载均衡

mysql负载均衡 安装mysql yum install -y mariadb-server systemctl start mariadb systemctl enable mariadb ss -ntl创建数据库并授权 MariaDB [(none)]> create database wordpress; Query OK, 1 row affected (0.00 sec)MariaDB [(none)]> grant all privileges o…