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

news2025/3/16 17:45:31

原子类和 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 次。
     */
    @Override
    public void run() {
        // 循环 10000 次,每次将 value 加 1
        for (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 次递增操作。
     */
    @Override
    public 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 次累加操作。
     */
    @Override
    public 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.coloradmin.cn/o/2316174.html

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

相关文章

【数据结构】数据结构,算法 概念

0.本篇问题&#xff1a; 数据、数据元素、数据对象、数据项之间的基本关系&#xff1f;ADT是什么&#xff1f;数据结构的三要素&#xff1f;数据的逻辑结构有哪些&#xff1f;数据的存储结构有哪些&#xff1f;算法的五个特征&#xff1f;O(1) O(logn) O(n^n) O(n) O(n^2…

总结 HTTP 协议的基本格式, 相关知识以及抓包工具fiddler的使用

目录 1 HTTP是什么 2 HTTP协议格式 3 HTTP请求(Request) 3.1 认识URL 3.2 方法 3.3 认识请求"报头"(header) 4 HTTP响应详解 4.1 认识"状态码"(statuscode) 4.2 认识响应"报头"(header) 4.3 认识响应"正⽂"(body) 5 通过f…

探索Maas平台与阿里 QWQ 技术:AI调参的魔法世界

摘要&#xff1a;本文介绍了蓝耘 Maas 平台在人工智能领域的表现及其核心优势&#xff0c;包括强大的模型支持、高效的资源调度和友好的操作界面。文章还探讨了蓝耘 Maas 平台与阿里 QWQ 技术的融合亮点及应用拓展实例&#xff0c;并提供了调参实战指南&#xff0c;最后对蓝耘 …

Linux第三次练习

1、创建根目录结构中的所有的普通文件 首先在根目录下面新创建一个test目录&#xff0c;然后将查找到的普通文件新建到test目录下 2、列出所有账号的账号名 3、将/etc/passwd中内容按照冒号隔开的第三个字符从大到小排序后输出所有内容 4、列出/etc/passwd中的第20行-25行内容…

软件测试知识总结

1、黑盒测试、白盒测试、灰盒测试 1.1 黑盒测试 黑盒测试又叫功能测试、数据驱动测试 或 基于需求规格说明书的功能测试。该类测试注重于测试软件的功能性需求。 采用这种测试方法&#xff0c;测试工程师把测试对象看作一个黑盒子&#xff0c;完全不考虑程序内部的逻辑结构和…

【HTML】三、表单与布局标签

文章目录 1、input1.1 input的占位文案1.2 单选框1.3 上传文件1.4 多选框 2、 下拉菜单3、文本域&#xff1a;多行输入4、label标签&#xff1a;说明与增大点击范围5、按钮与form表单6、无语义布局标签7、有语义的布局标签8、字符实体9、练习&#xff1a;注册页面 1、input in…

【结构设计】3D打印创想三维Ender 3 v2

【结构设计】3D打印创想三维Ender 3 v2 文章目录 前言一、Creality Slicer1.2.3打印参数设置二、配件更换1.捆扎绑扎线2.气动接头3D打印机配件插头3.3D打印机配件Ender3pro/V2喷头套件4.读卡器 TF卡5.micro sd卡 三、调平四、参考文章总结 前言 使用工具&#xff1a; 1.创想三…

UE小:UE5.5 PixelStreamingInfrastructure 使用时注意事项

1、鼠标默认显示 player.ts中的Config中添加HoveringMouse:true 然后运行typescript\package.json中的"build":npx webpack --config webpack.prod.js

Anaconda 入门指南

Anaconda 入门指南 一、下载安装 Anaconda 1、下载地址&#xff1a;Anaconda 推荐下载 python3 版本, 毕竟未来 python2 是要停止维护的。 2、安装 Anaconda 按照安装程序提示一步步安装就好了, 安装完成之后会多几个应用&#xff1a; Anaconda Navigtor &#xff1a;用于管…

web组态可视化编辑器

Web组态可视化编辑器是一种用于创建和配置工业自动化、物联网&#xff08;IoT&#xff09;和智能建筑等领域的图形化用户界面&#xff08;GUI&#xff09;的工具。它允许用户通过拖放组件、配置参数和连接数据源来设计和部署实时监控和控制界面。以下是一些常见的Web组态可视化…

CTA重建:脑血管重建,CT三维重建,三维建模 技术,实现

CTA&#xff08;CT血管造影&#xff09;是一种基于CT扫描的医学成像技术&#xff0c;主要用于血管系统的三维重建和可视化。脑血管重建是CTA的重要应用之一&#xff0c;能够帮助医生诊断脑血管疾病&#xff08;如动脉瘤、狭窄、畸形等&#xff09;。以下是实现CTA脑血管重建、C…

Ollama+OpenWebUI本地部署大模型

OllamaOpenWebUI本地部署大模型 前言Ollama使用Ollama安装Ollama修改配置Ollama 拉取远程大模型Ollama 构建本地大模型Ollama 运行本地模型&#xff1a;命令行交互Api调用Web 端调用 总结 前言 Ollama是一个开源项目&#xff0c;用于在本地计算机上运行大型语言模型&#xff0…

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

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

Vue 自定义指令深度解析与应用实践

文章目录 1. 自定义指令概述1.1 核心概念1.2 指令生命周期 2. 自定义指令基础2.1 指令注册2.2 指令使用 3. 指令钩子函数详解3.1 钩子函数参数3.2 钩子函数示例 4. 自定义指令应用场景4.1 表单自动聚焦4.2 权限控制4.3 图片懒加载 5. 高级应用技巧5.1 动态指令参数5.2 指令修饰…

Android 手机启动过程

梳理 为了梳理思路&#xff0c;笔者画了一幅关于 Android 手机启动的过程图片内容纯属个人见解&#xff0c;如有错误&#xff0c;欢迎各位指正

Unity 开发资源汇总 | 插件 | 模型 | 源码(不断更新中,建议收藏)

&#x1f493; 欢迎访问 Unity 打怪升级大本营 Unity是一个强大的游戏开发平台&#xff0c;它提供了丰富的工具和资源&#xff0c;让开发者能够创造出令人惊叹的游戏和交互式体验。无论你是初学者还是经验丰富的开发者&#xff0c;Unity的生态系统中总有一些资源可以帮助你提升…

JVM崩溃时产生的文件 hs_err.pid.log

hs_err.pid.log hs_err.pid.log&#xff1a;当jvm崩溃时&#xff0c;会生成一个hs_err_pid.log文件&#xff0c;并且把它存放到程序目录下&#xff0c;可以通过该文件来定位导致jvm崩溃的原因。 jvm崩溃&#xff0c;是由jvm自身的bug或者本地方法执行错误引起的&#xff0c;本…

聊聊 Redis 的一些有趣的特性(上)

聊聊 Redis 的一些有趣的特性&#xff08;上&#xff09; 一、持久化 Redis 是内存数据库&#xff0c;数据全部保存在内存中。如果服务器发生宕机&#xff0c;内存中的数据将会全部丢失。为防止系统崩溃后数据丢失&#xff0c;Redis 提供了持久化功能&#xff0c;可将内存中的…

使用OpenCV和MediaPipe库——抽烟检测(姿态监控)

目录 抽烟检测的运用 1. 安全监控 (1) 公共场所禁烟监管 (2) 工业安全 2. 智能城市与执法 (1) 城市违章吸烟检测 (2) 无人值守管理 3. 健康管理与医疗 (1) 吸烟习惯分析 (2) 远程监护 4. AI 监控与商业分析 (1) 保险行业 (2) 商场营销 5. 技术实现 (1) 计算机视…

怎么有效降低知网AIGC率

在学术创作日益规范且数字化检测技术不断发展的当下&#xff0c;知网 AIGC 检测成为了众多创作者关注的焦点。许多人苦恼于如何有效降低知网 AIGC 率&#xff0c;让自己的作品在通过检测的同时&#xff0c;彰显出真实的创作水平与独特性。接下来&#xff0c;我们就深入探讨降低…