Volatile不保证原子性及解决方案

简介: **原子性在并发编程中确保操作不可中断,保持数据一致性。volatile保证可见性但不保证原子性,如`count++`在多线程环境下仍可能导致数据不一致。解决方案包括使用`synchronized`、`AtomicInteger`或`ReentrantLock`来确保复合操作的原子性和线程安全。例子展示了volatile在并发自增中的局限性,实际值通常小于预期,强调了正确选择同步机制的重要性。**

原子性的意义

原子性特别是在并发编程领域,是一个极其重要的概念,原子性指的是一个操作或一组操作要么全部执行成功,要么全部不执行,不会出现部分执行的情况。这意味着原子性操作是不可分割的,它们在执行过程中不会被其他操作中断或干扰。

原子性的意义在于它保证了数据的一致性和程序的正确性。在多线程或多进程的环境中,当多个操作同时访问和修改共享数据时,如果没有原子性保证,可能会导致数据不一致或不确定的结果。例如,如果一个线程在读取某个数据时,另一个线程同时修改了这个数据,那么第一个线程读取到的数据可能是不正确的。通过确保操作的原子性,可以避免这种情况,从而维护数据的完整性和程序的正确执行。

了解了上面的原子性的重要概念后,接下来一起聊一聊 volatile 关键字。

volatile 关键字在 Java 中用于确保变量的更新对所有线程都是可见的,但它并不保证复合操作的原子性。这意味着当多个线程同时访问一个 volatile 变量时,可能会遇到读取不一致的问题,尽管它们不会看到部分更新的值。

Volatile 的限制

  • 不保证原子性:volatile 变量的单个读写操作是原子的,但复合操作(如自增或同步块)不是原子的。
  • 不保证顺序性:volatile 变量的读写操作不会与其他操作(如非 volatile 变量的读写)发生重排序。

一个例子

用一个示例来解释会更清楚点,假如我们有一段代码是这样的:

class Counter {
   
    private volatile int count = 0;

    void increment() {
   
        count++;
    }

    int getCount() {
   
        return count;
    }
}

尽管 count 是 volatile 变量,但 increment 方法中的复合操作 count++(读取-增加-写入)不是原子的。因此,在多线程环境中,多个线程可能会同时读取相同的初始值,然后增加它,导致最终值低于预期。

volatile 不保证原子性的代码验证

以下是一个简单的 Java 程序,演示了 volatile 变量在多线程环境中不保证复合操作原子性的问题:


public class VolatileTest {
   
    private static volatile int counter = 0;

    public static void main(String[] args) throws InterruptedException {
   
        int numberOfThreads = 10000;
        Thread[] threads = new Thread[numberOfThreads];

        for (int i = 0; i < numberOfThreads; i++) {
   
            threads[i] = new Thread(() -> {
   
                for (int j = 0; j < 100; j++) {
   
                    counter++;
                }
            });
            threads[i].start();
        }

        for (int i = 0; i < numberOfThreads; i++) {
   
            threads[i].join();
        }

        System.out.println("Expected count: " + (numberOfThreads * 100));
        System.out.println("Actual count: " + counter);
    }
}

在这个例子中:

  • counter 是一个 volatile 变量。
  • 每个线程都会对 counter 执行 100 次自增操作。
  • 理论上,如果 counter++ 是原子的,最终的 counter 值应该是 10000 * 100。

然而,由于 counter++ 包含三个操作:读取 counter 的值、增加 1、写回 counter 的值,这些操作不是原子的。因此,在多线程环境中,最终的 counter 值通常会小于预期值,这证明了 volatile 变量不保证复合操作的原子性。

解决方案

1. 使用 synchronized 方法或块:

  • 将访问 volatile 变量的方法或代码块声明为 synchronized,确保原子性和可见性。
class Counter {
   
    private volatile int count = 0;

    synchronized void increment() {
   
        count++;
    }

    synchronized int getCount() {
   
        return count;
    }
}

2. 使用 AtomicInteger 类:

java.util.concurrent.atomic 包中的 AtomicInteger 提供了原子操作,可以替代 volatile 变量。


import java.util.concurrent.atomic.AtomicInteger;

class Counter {
   
    private AtomicInteger count = new AtomicInteger(0);

    void increment() {
   
        count.incrementAndGet();
    }

    int getCount() {
   
        return count.get();
    }
}

3. 使用锁(如 ReentrantLock):

使用显式锁(如 ReentrantLock)来同步访问 volatile 变量的代码块。


import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Counter {
   
    private volatile int count = 0;
    private final Lock lock = new ReentrantLock();

    void increment() {
   
        lock.lock();
        try {
   
            count++;
        } finally {
   
            lock.unlock();
        }
    }

    int getCount() {
   
        lock.lock();
        try {
   
            return count;
        } finally {
   
            lock.unlock();
        }
    }
}

使用volatile变量的正确使用场景

如果操作是简单的读写,并且你只需要保证可见性,可以使用 volatile。但对于复合操作,可以使用上述其他方法来实现,通过这些方法,可以确保在多线程环境中对共享资源的正确同步和可见性。

相关文章
|
关系型数据库 MySQL 数据挖掘
MySQL - binlog同步过程
MySQL - binlog同步过程
763 0
|
编解码
Jetson 学习笔记(十四):降低远程操控延时-修改屏幕分辨率
这篇文章是关于如何通过修改屏幕分辨率来降低Jetson Nano在远程操控时的延时。
438 3
Jetson 学习笔记(十四):降低远程操控延时-修改屏幕分辨率
|
9月前
|
数据采集 机器学习/深度学习 人工智能
全球十亿级轨迹点驱动,首个轨迹基础大模型来了
在人工智能和大数据背景下,南方科技大学等机构提出了一种名为UniTraj的新型轨迹基础大模型。该模型通过学习全球大规模、高质量的轨迹数据,实现对人类移动模式的普遍适用性和可扩展性。UniTraj具备任务自适应性、区域独立性和数据质量鲁棒性的特点,解决了现有方法的任务特定性、区域依赖性和数据敏感性问题。研究人员还构建了包含2.45亿条轨迹的WorldTrace数据集,以支持模型训练和评估。实验结果表明,UniTraj在多个轨迹分析任务上表现出显著优势,为轨迹建模领域带来重要突破。论文地址:https://arxiv.org/pdf/2411.03859。
403 6
|
Java 数据库
数据原子性
数据原子性
545 3
|
C++
QT第一个程序命名空间详解,解释ui_widget的和xxx.cpp的联系
QT第一个程序命名空间详解,解释ui_widget的和xxx.cpp的联系
473 0
|
资源调度 JavaScript 前端开发
window系统如何管理多版本node
window系统如何管理多版本node
311 0
|
监控 Java 数据安全/隐私保护
性能监控之 JMX 监控 Docker 容器中的 Java 应用
【6月更文挑战9天】性能监控之 JMX 监控 Docker 容器中的 Java 应用
971 1
|
Java API
第3关:HDFS-JAVA接口之上传文件
第3关:HDFS-JAVA接口之上传文件
1221 0
第3关:HDFS-JAVA接口之上传文件
|
前端开发 程序员
前端的开发规范
前端的开发规范
1353 0
|
并行计算 安全 Java
如何使用Java进行异步编程
如何使用Java进行异步编程