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

server/2025/3/16 18:54:06/

原子类和 volatile异同

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

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

java">/*** 该类用于演示 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 关键字来解决一个经典的线程安全问题,给出具体的代码对比,然后再分析它们背后的区别。

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

java">/*** 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,例如我执行的结果如下:

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

java">/*** 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 来解决这个问题,代码如下所示:

java">/*** 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.ppmy.cn/server/175498.html

相关文章

【设计模式】遍历集合的艺术:深入探索迭代器模式的无限可能

概述 定义&#xff1a;提供一个对象来顺序访问聚合对象中的一系列数据&#xff0c;而不暴露聚合对象的内部表示。 结构 迭代器模式主要包含以下角色&#xff1a; 抽象聚合&#xff08;Aggregate&#xff09;角色&#xff1a;定义存储、添加、删除聚合元素以及创建迭代器对象…

Jump Desktop for Mac v9.0.94 优秀的远程桌面连接工具 支持M、Intel芯片

Jump Desktop for Mac 版是macOS平台上的一款远程控制程序&#xff0c;支持Windows和Mac 双平台&#xff0c;通过邮件关联即可帮助设备自动找到桌面并进行操作。 应用介绍 Jump Desktop for Mac 是一款Mac上非常强大和易用的远程桌面控制软件&#xff0c;支持RDP、VNC协议&am…

Windows-PyQt5安装+PyCharm配置QtDesigner + QtUIC

个人环境 Windows 11 pycharm 2024.2 Anaconda2024.6python 3.9 1)先使用pip命令在线安装 1)pip install PyQt5 2)pip install PyQt5-tools2)配置环境变量 1&#xff1a;安装成功后可以在python的安装目录Lib\site-packahes目录下看到安装包。比如我的路径是E:\anaconda3…

[Failed to change to remote directory [D:/web/]]

这里写自定义目录标题 jenkins使用 Publish Over SSH发布到 windows服务器报错&#xff1a; Failed to connect and initialize SSH connection. Message: [Failed to change to remote directory [D:/web/]] 解决办法 将远程目录改为 /D:/web/

export、export default 和 module.exports 深度解析

文章目录 1. 模块系统概述1.1 模块系统对比1.2 模块加载流程 2. ES Modules2.1 export 使用2.2 export default 使用2.3 混合使用 3. CommonJS3.1 module.exports 使用3.2 exports 使用 4. 对比分析4.1 语法对比4.2 使用场景 5. 互操作性5.1 ES Modules 中使用 CommonJS5.2 Com…

【蓝桥杯—单片机】第十五届省赛真题代码题解析 | 思路整理

第十五届省赛真题代码题解析 前言赛题代码思路笔记竞赛板配置建立模板明确基本要求显示功能部分频率界面正常显示高位熄灭 参数界面基础写法&#xff1a;两个界面分开来写优化写法&#xff1a;两个界面合一起写 时间界面回显界面校准校准过程校准错误显示 DAC输出部分按键功能部…

【免费】1949-2020年各省人均GDP数据

1949-2020年各省人均GDP数据 1、时间&#xff1a;1952-2020年 2、来源&#xff1a;国家统计局、统计年鉴 3、指标&#xff1a;各省人均GDP 4、范围&#xff1a;31省 5、指标解释&#xff1a;人均GDP&#xff08;Gross Domestic Product per capita&#xff09;是指一个国家…

生态安全的范式

生态安全是一个内涵很大的概念。在不同的场景下&#xff0c;其解读不太一致。一般来说&#xff0c;存在两种解读。一是防止由于生态环境的退化对经济基础构成威胁&#xff0c;主要指环境质量状况低劣和自然资源的减少以及退化削弱了经济可持续发展的支撑能力&#xff1b;二是防…