Java多线程与高并发专题——原子类和 volatile、synchronized 有什么异同?

原子类和 volatile异同

首先,通过我们对原子类和的了解,原子类和volatile 都能保证多线程环境下的数据可见性。在多线程程序中,每个线程都有自己的工作内存,当多个线程访问共享变量时,可能会出现一个线程修改了共享变量的值,而其他线程不能及时看到最新值的情况。原子类和volatile关键字都能在一定程度上解决这个问题。例如,当一个变量被volatile修饰后,对该变量的写操作会立即刷新到主内存,读操作会直接从主内存读取,保证了其他线程能看到最新的值;原子类同样可以保证对变量操作的结果能被其他线程及时看到。

下面我们通过一个代码去看看它们的差异:

/*** 该类用于演示 volatile 关键字和 AtomicInteger 类在多线程环境下的不同表现。* 展示了使用 volatile 变量和 AtomicInteger 类进行自增操作的差异。*/
public class VolatileVsAtomic {// 用 volatile 修饰的变量,保证变量的可见性,但不保证操作的原子性private static volatile int volatileCount = 0;// 原子类,提供原子操作,保证操作的原子性private static AtomicInteger atomicCount = new AtomicInteger(0);/*** 主方法,程序的入口点。* 创建多个线程,分别对 volatile 变量和 AtomicInteger 类的实例进行自增操作,并输出结果。** @param args 命令行参数* @throws InterruptedException 如果线程在等待时被中断*/public static void main(String[] args) throws InterruptedException {// 定义线程数量int threadCount = 10;// 创建线程数组Thread[] threads = new Thread[threadCount];// 使用 volatile 变量进行自增操作for (int i = 0; i < threadCount; i++) {// 创建线程threads[i] = new Thread(() -> {// 每个线程执行 1000 次自增操作for (int j = 0; j < 1000; j++) {// 此操作不是原子性的,可能会出现数据竞争问题volatileCount++;}});// 启动线程threads[i].start();}// 等待所有线程执行完毕for (Thread thread : threads) {thread.join();}// 输出 volatile 变量的最终值System.out.println("Volatile count: " + volatileCount);// 重置计数器volatileCount = 0;atomicCount.set(0);// 使用原子类进行自增操作for (int i = 0; i < threadCount; i++) {// 创建线程threads[i] = new Thread(() -> {// 每个线程执行 1000 次自增操作for (int j = 0; j < 1000; j++) {// 原子性自增操作,保证操作的原子性atomicCount.incrementAndGet();}});// 启动线程threads[i].start();}// 等待所有线程执行完毕for (Thread thread : threads) {thread.join();}// 输出 AtomicInteger 类实例的最终值System.out.println("Atomic count: " + atomicCount.get());}
}

 输出结果如下:

在上述代码中,volatileCount是一个被volatile修饰的变量,多个线程对其进行自增操作时,由于自增操作不是原子性的,最终结果可能小于预期值;而atomicCount是一个AtomicInteger类型的原子类,多个线程对其进行自增操作时,能保证操作的原子性,最终结果是准确的。

原子类和 volatile 的使用场景

那下面我们就来说一下原子类和 volatile 各自的使用场景。

我们可以看出,volatile 和原子类的使用场景是不一样的,如果我们有一个可见性问题,那么可以使用 volatile 关键字,但如果我们的问题是一个组合操作,需要用同步来解决原子性问题的话,那么可以使用原子变量,而不能使用 volatile 关键字。

通常情况下,volatile 可以用来修饰 boolean 类型的标记位,因为对于标记位来讲,直接的赋值操作本身就是具备原子性的,再加上 volatile 保证了可见性,那么就是线程安全的了。

而对于会被多个线程同时操作的计数器 Counter 的场景,这种场景的一个典型特点就是,它不仅仅是一个简单的赋值操作,而是需要先读取当前的值,然后在此基础上进行一定的修改,再把它给赋值回去。这样一来,我们的 volatile 就不足以保证这种情况的线程安全了。我们需要使用原子类来保证线程安全。

原子类和 synchronized异同

原子类和 synchronized 关键字都可以用来保证线程安全,下面我们分别用原子类和 synchronized 关键字来解决一个经典的线程安全问题,给出具体的代码对比,然后再分析它们背后的区别。

首先,原始的线程不安全的情况的代码如下所示:

/*** BaseTest 类实现了 Runnable 接口,用于演示多线程并发修改共享变量的情况。* 该类包含一个静态变量 value,多个线程会同时对其进行递增操作。*/
public class BaseTest implements Runnable{// 静态变量 value,用于存储线程递增的结果static int value = 0;/*** main 方法是程序的入口点,创建并启动两个线程来执行 BaseTest 实例的 run 方法。* 等待两个线程执行完毕后,打印最终的 value 值。* * @param args 命令行参数* @throws InterruptedException 如果线程在等待过程中被中断*/public static void main(String[] args) throws InterruptedException {// 创建 BaseTest 实例Runnable runnable = new BaseTest();// 创建第一个线程并传入 BaseTest 实例Thread thread1 = new Thread(runnable);// 创建第二个线程并传入 BaseTest 实例Thread thread2 = new Thread(runnable);// 启动第一个线程thread1.start();// 启动第二个线程thread2.start();// 等待第一个线程执行完毕thread1.join();// 等待第二个线程执行完毕thread2.join();// 打印最终的 value 值System.out.println(value);}/*** run 方法是 Runnable 接口的实现,包含一个循环,将 value 变量递增 10000 次。*/@Overridepublic void run() {// 循环 10000 次,每次将 value 加 1for (int i = 0; i < 10000; i++) {value++;}}
}

在代码中我们新建了一个 value 变量,并且在两个线程中对它进行同时的自加操作,每个线程加 10000次,然后我们用 join 来确保它们都执行完毕,最后打印出最终的数值。

因为 value++ 不是一个原子操作,所以上面这段代码是线程不安全的,所以代码的运行结果会小于 20000,例如我执行的结果如下:

我们首先给出方法一,也就是用原子类来解决这个问题,代码如下所示:

/*** AtomicTest 类实现了 Runnable 接口,用于演示使用 AtomicInteger 进行线程安全的计数操作。* 该类创建了两个线程,每个线程都会对一个静态的 AtomicInteger 实例进行 10000 次递增操作。* 最后,主线程等待两个子线程执行完毕,并输出最终的计数值。*/
public class AtomicTest implements Runnable {// 静态的 AtomicInteger 实例,用于线程安全的计数操作static AtomicInteger atomicInteger = new AtomicInteger();/*** 程序的入口点,创建并启动两个线程,等待它们执行完毕,然后输出最终的计数值。** @param args 命令行参数,在本程序中未使用。* @throws InterruptedException 如果在等待线程执行完毕时被中断。*/public static void main(String[] args) throws InterruptedException {// 创建一个 AtomicTest 实例,作为线程的任务Runnable runnable = new AtomicTest();// 创建第一个线程并传入任务Thread thread1 = new Thread(runnable);// 创建第二个线程并传入任务Thread thread2 = new Thread(runnable);// 启动第一个线程thread1.start();// 启动第二个线程thread2.start();// 等待第一个线程执行完毕thread1.join();// 等待第二个线程执行完毕thread2.join();// 输出最终的计数值System.out.println(atomicInteger.get());}/*** 实现 Runnable 接口的 run 方法,该方法会对 atomicInteger 进行 10000 次递增操作。*/@Overridepublic void run() {// 循环 10000 次,每次对 atomicInteger 进行递增操作for (int i = 0; i < 10000; i++) {// 原子地递增 atomicInteger 的值并返回更新后的值atomicInteger.incrementAndGet();}}
}

用原子类之后,我们的计数变量就不再是一个普通的 int 变量了,而是 AtomicInteger 类型的对象,并且自加操作也变成了 incrementAndGet 法。由于原子类可以确保每一次的自加操作都是具备原子性的,所以这段程序是线程安全的,所以以上程序的运行结果会始终等于 20000。

下面我们给出方法二,我们用 synchronized 来解决这个问题,代码如下所示:

/*** SynTest 类用于演示多线程环境下的同步机制。* 该类实现了 Runnable 接口,多个线程可以共享同一个实例来执行任务。* 通过同步块确保对静态变量 value 的安全访问。*/
public class SynTest  implements Runnable {// 静态变量,用于记录所有线程累加的结果static int value = 0;/*** 程序的入口点,创建并启动两个线程来执行任务。** @param args 命令行参数* @throws InterruptedException 如果线程在等待时被中断*/public static void main(String[] args) throws InterruptedException {// 创建 SynTest 类的实例Runnable runnable = new SynTest();// 创建第一个线程并传入 Runnable 实例Thread thread1 = new Thread(runnable);// 创建第二个线程并传入 Runnable 实例Thread thread2 = new Thread(runnable);// 启动第一个线程thread1.start();// 启动第二个线程thread2.start();// 等待第一个线程执行完毕thread1.join();// 等待第二个线程执行完毕thread2.join();// 输出最终累加结果System.out.println(value);}/*** 实现 Runnable 接口的 run 方法,定义线程要执行的任务。* 在这个方法中,线程会对静态变量 value 进行 10000 次累加操作。*/@Overridepublic void run() {// 循环 10000 次for (int i = 0; i < 10000; i++) {// 使用同步块确保同一时间只有一个线程可以访问和修改 value 变量synchronized (this) {// 对 value 变量进行累加操作value++;}}}
}

它与最开始的线程不安全的代码的区别在于,在 run 方法中加了 synchronized 代码块,就可以非常轻松地解决这个问题,由于 synchronized 可以保证代码块内部的原子性,所以以上程序的运行结果也始终等于 20000,是线程安全的。

原子类和 synchronized 的使用对比

下面我们就对这两种不同的方案进行分析。

第一点,我们来看一下它们背后原理的不同。

synchronized 保证线程安全的核心是 monitor 锁,同步方法和同步代码块的背后原理会有少许差异,但总体思想是一致的:在执行同步代码之前,需要首先获取到 monitor 锁,执行完毕后,再释放锁。而原子类保证线程安全的原理是利用了 CAS 操作。从这一点上看,虽然原子类和 synchronized 都能保证线程安全,但是其实现原理是大有不同的。

第二点不同是使用范围的不同。

对于原子类而言,它的使用范围是比较局限的。因为一个原子类仅仅是一个对象,不够灵活。而synchronized 的使用范围要广泛得多。比如说 synchronized 既可以修饰一个方法,又可以修饰一段代码,相当于可以根据我们的需要,非常灵活地去控制它的应用范围。

所以仅有少量的场景,例如计数器等场景,我们可以使用原子类。而在其他更多的场景下,如果原子类不适用,那么我们就可以考虑用 synchronized 来解决这个问题。

第三个区别是粒度的区别。

原子变量的粒度是比较小的,它可以把竞争范围缩小到变量级别。通常情况下,synchronized 锁的粒度都要大于原子变量的粒度。如果我们只把一行代码用 synchronized 给保护起来的话,有一点杀鸡焉用牛刀的感觉。

第四点是它们性能的区别,同时也是悲观锁和乐观锁的区别。

因为 synchronized 是一种典型的悲观锁,而原子类恰恰相反,它利用的是乐观锁。所以,我们在比较synchronized 和 AtomicInteger 的时候,其实也就相当于比较了悲观锁和乐观锁的区别。

从性能上来考虑的话,悲观锁的操作相对来讲是比较重量级的。因为 synchronized 在竞争激烈的情况下,会让拿不到锁的线程阻塞,而原子类是永远不会让线程阻塞的。不过,虽然 synchronized 会让线程阻塞,但是这并不代表它的性能就比原子类差。

因为悲观锁的开销是固定的,也是一劳永逸的。随着时间的增加,这种开销并不会线性增长。而乐观锁虽然在短期内的开销不大,但是随着时间的增加,它的开销也是逐步上涨的。

所以从性能的角度考虑,它们没有一个孰优孰劣的关系,而是要区分具体的使用场景。在竞争非常激烈的情况下,推荐使用 synchronized;而在竞争不激烈的情况下,使用原子类会得到更好的效果。

值得注意的是,synchronized 的性能随着 JDK 的升级,也得到了不断的优化。synchronized 会从无锁升级到偏向锁,再升级到轻量级锁,最后才会升级到让线程阻塞的重量级锁。因此synchronized 在竞争不激烈的情况下,性能也是不错的。

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

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

相关文章

c语言笔记 作用域

目录 作用域的基本概念 1.函数声明的作用域 2.局部变量的作用域 3.全局作用域 4.static修饰后的作用域 作用域的基本概念 在c语言中&#xff0c;我们的标志符是具有一定的可见范围的&#xff0c;我们称这个可见范围为作用域 在软件开发中&#xff0c;我们要确定好标识符的作…

MySQL数据库知识总结

MySQL数据库知识总结 一、基本概念及其介绍二、数据库中的数据类型&#xff08;一&#xff09;数值类型&#xff08;二&#xff09;字符串类型&#xff08;三&#xff09;日期类型 三、数据库基础语法&#xff08;一&#xff09;数据库的常用操作&#xff08;二&#xff09;数据…

SpaceSync智能排班:重构未来办公空间的神经中枢

文心智能体平台可免费使用DeepSeek 满血版啦&#xff0c;使用DeepSeek模型创建并提交智能体&#xff0c;即有机会瓜分万元奖金&#xff01;有这等好事还不快冲&#xff01; 文心智能体官网&#xff1a;文心智能体平台AgentBuilder | 想象即现实 本片文章为作者参加文心智能体平…

Blender-MCP服务源码3-插件开发

Blender-MCP服务源码3-插件开发 Blender-MCP服务源码解读-如何进行Blender插件开发 1-核心知识点 1&#xff09;使用Blender开发框架学习如何进行Blender调试2&#xff09;学习目标1-移除所有的Blender业务-了解如何MCP到底做了什么&#xff1f;3&#xff09;学习目标2-模拟MC…

每日一题---dd爱框框(Java中输入数据过多)

dd爱框框 实例&#xff1a; 输入&#xff1a; 10 20 1 1 6 10 9 3 3 5 3 7 输出&#xff1a; 3 5 这道题要解决Java中输入的数过多时&#xff0c;时间不足的的问题。 应用这个输入模板即可解决&#xff1a; Java中输入大量数据 import java.util.*; import java.io.*;pu…

Qlik Sense New Install with Restore

Background In case you meet the upgrade issue like us , you can follow the below step to recover the existing data to new installed Qlik Sense . Powered by Moshow郑锴-CSDN博客 please follow below steps: pgsql dump backupbackup table into sql by DBeaverst…

大数据-spark3.5安装部署之standalone模式

真实工作中还是要将应用提交到集群中去执行&#xff0c;Standalone模式就是使用Spark自身节点运行的集群模式&#xff0c;体现了经典的master-slave模式。集群共三台机器&#xff0c;具体如下 u22server4spark&#xff1a; master worker u22server4spark2&#xff1a; worke…

Uniapp 开发 App 端上架用户隐私协议实现指南

文章目录 引言一、为什么需要用户隐私协议&#xff1f;二、Uniapp 中实现用户隐私协议的步骤2.1 编写隐私协议内容2.2 在 Uniapp 中集成隐私协议2.3 DCloud数据采集说明2.4 配置方式3.1 Apple App Store3.2 Google Play Store 四、常见问题与解决方案4.1 隐私协议内容不完整4.2…

【C++】 —— 笔试刷题day_5

刷题day_5 一、游游的you 题目链接&#xff1a;游游的you 题目解析 题目要求&#xff1a; 输入a&#xff0c;b&#xff0c;c表示y、o、u三个字母的个数&#xff1b; 将这些字母连成字符串&#xff0c;并且这里you三个字母相邻获得2分&#xff0c;两个o字母相邻获得1分。 让我…

78. Harmonyos NEXT 懒加载数据源实现解析:BasicDataSource与CommonLazyDataSourceModel详解

温馨提示&#xff1a;本篇博客的详细代码已发布到 git : https://gitcode.com/nutpi/HarmonyosNext 可以下载运行哦&#xff01; Harmonyos NEXT 懒加载数据源实现解析&#xff1a;BasicDataSource与CommonLazyDataSourceModel详解 文章目录 Harmonyos NEXT 懒加载数据源实现解…

如何打包数据库mysql数据,并上传到虚拟机上进行部署?

1.连接数据库&#xff0c;使得我们能看到数据库信息&#xff0c;才能进行打包上传 2. 3. 导出结果如下&#xff0c;是xml文件 4.可以查询每个xml文件的属性&#xff0c;确保有大小&#xff0c;这样才是真实导出 5跟着黑马&#xff0c;新建文件夹&#xff0c;并且把对应的东西放…

Springboot+mabatis增删改查,设置不可重复字段

今天又学会了一个操作&#xff0c;我们数据库中&#xff0c;可能要求一个字段名字不可以重复&#xff0c;我们就进行这样的操作&#xff01;设计表&#xff0c;然后点击索引&#xff0c;选择字段&#xff0c;加入索引类型和索引方法&#xff0c;然后ctrlS保存!即可 如果一旦还…

C# NX二次开发:矩形阵列和线性阵列等多种方法讲解

大家好&#xff0c;今天讲一些关于阵列相关的UFUN函数。 UF_MODL_create_linear_iset (view source)&#xff1a;这个函数为创建矩形阵列。 intmethodInputMethod: 0 General 1 Simple 2 Identicalchar *number_in_xInputNumber in XC direction.char *distance_xInputSpac…

嵌入式硬件: GPIO与二极管基础知识详解

1. 前言 在嵌入式系统和硬件开发中&#xff0c;GPIO&#xff08;通用输入输出&#xff09;是至关重要的控制方式&#xff0c;而二极管作为基础电子元件&#xff0c;广泛应用于信号整流、保护电路等。本文将从基础原理出发&#xff0c;深入解析GPIO的输入输出模式&#xff0c;包…

CTF--Web安全--SQL注入之报错注入

CTF–Web安全–SQL注入之报错注入 一、报错注入的概念 用户使用数据库查询语句&#xff0c;向数据库发送错误指令&#xff0c;数据库返回报错信息&#xff0c;报错信息中参杂着我们想要获取的隐私数据。通常在我们在页面显示中找不到回显位的时候&#xff0c;使用报错注入。 二…

matlab 模糊pid实现温度控制

1、内容简介 matlab162-模糊pid实现温度控制 可以交流、咨询、答疑 2、内容说明 略基于PID电加热炉温度控制系统设计 摘要 电加热炉随着科学技术的发展和工业生产水平的提高&#xff0c;已经在冶金、化工、 机械等各类工业控制中得到了广泛应用&#xff0c;并且在国民经济中占…

RabbitMq C++客户端的使用

1.RabbitMq介绍 RabbitMQ 是一款开源的消息队列中间件&#xff0c;基于 AMQP&#xff08;高级消息队列协议&#xff09;实现&#xff0c;支持多种编程语言和平台。以下是其核心特点和介绍&#xff1a; 核心特点 多语言支持 提供 Java、Python、C#、Go、JavaScript 等语言的客…

星越L_备胎更换/千斤顶使用讲解

目录 1.车辆停靠在坚实平坦的路面上。 2.打开危险警示灯、 3.设立三角指示牌 4.取出备胎及随车工具 5.使用螺栓扳手对每个螺母进行松动 6使用千斤顶抬升 7、其他 轮胎漏气或爆胎的情况,需要使用千斤顶更换备胎 1.车辆停靠在坚实平坦的路面上。 2.打开危险警示灯、

【Python 数据结构 15.哈希表】

目录 一、哈希表的基本概念 1.哈希表的概念 2.键值对的概念 3.哈希函数的概念 4.哈希冲突的概念 5.常用的哈希函数 Ⅰ、直接定址法 Ⅱ、平方取中法 Ⅲ、折叠法 Ⅳ、除留余数法 Ⅴ、位与法 6.哈希冲突的解决方案 Ⅰ、开放定址法 Ⅱ、链地址法 7.哈希表的初始化 8.哈希表的元素插…

软件测试之测试分类

1. 为什么要对软件测试进行分类 软件测试是软件⽣命周期中的⼀个重要环节&#xff0c;具有较⾼的复杂性&#xff0c;对于软件测试&#xff0c;可以从不同的⻆度 加以分类&#xff0c;使开发者在软件开发过程中的不同层次、不同阶段对测试⼯作进⾏更好的执⾏和管理测试 的分类⽅…