Volatile不保证原子性及办理方案

打印 上一主题 下一主题

主题 880|帖子 880|积分 2640

原子性的意义

原子性特别是在并发编程领域,是一个极其告急的概念,原子性指的是一个操纵或一组操纵要么全部执行成功,要么全部不执行,不会出现部门执行的情况。这意味着原子性操纵是不可分割的,它们在执行过程中不会被其他操纵停止或干扰。
原子性的意义在于它保证了数据的一致性和程序的正确性。在多线程或多历程的环境中,当多个操纵同时访问和修改共享数据时,如果没有原子性保证,可能会导致数据不一致或不确定的结果。例如,如果一个线程在读取某个数据时,另一个线程同时修改了这个数据,那么第一个线程读取到的数据可能是不正确的。通过确保操纵的原子性,可以避免这种情况,从而维护数据的完备性和程序的正确执行。
相识了上面的原子性的告急概念后,接下来一起聊一聊 volatile 关键字。
volatile 关键字在 Java 中用于确保变量的更新对所有线程都是可见的,但它并不保证复合操纵的原子性。这意味着当多个线程同时访问一个 volatile 变量时,可能会遇到读取不一致的题目,尽管它们不会看到部门更新的值。
Volatile 的限制

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

用一个示例来解释会更清楚点,假如我们有一段代码是这样的:
  1. class Counter {
  2.     private volatile int count = 0;
  3.     void increment() {
  4.         count++;
  5.     }
  6.     int getCount() {
  7.         return count;
  8.     }
  9. }
复制代码
尽管 count 是 volatile 变量,但 increment 方法中的复合操纵 count++(读取-增加-写入)不是原子的。因此,在多线程环境中,多个线程可能会同时读取相同的初始值,然后增加它,导致最终值低于预期。
volatile 不保证原子性的代码验证

以下是一个简单的 Java 程序,演示了 volatile 变量在多线程环境中不保证复合操纵原子性的题目:
  1. public class VolatileTest {
  2.     private static volatile int counter = 0;
  3.     public static void main(String[] args) throws InterruptedException {
  4.         int numberOfThreads = 10000;
  5.         Thread[] threads = new Thread[numberOfThreads];
  6.         for (int i = 0; i < numberOfThreads; i++) {
  7.             threads[i] = new Thread(() -> {
  8.                 for (int j = 0; j < 100; j++) {
  9.                     counter++;
  10.                 }
  11.             });
  12.             threads[i].start();
  13.         }
  14.         for (int i = 0; i < numberOfThreads; i++) {
  15.             threads[i].join();
  16.         }
  17.         System.out.println("Expected count: " + (numberOfThreads * 100));
  18.         System.out.println("Actual count: " + counter);
  19.     }
  20. }
复制代码
在这个例子中:

  • counter 是一个 volatile 变量。
  • 每个线程都会对 counter 执行 100 次自增操纵。
  • 理论上,如果 counter++ 是原子的,最终的 counter 值应该是 10000 * 100。
然而,由于 counter++ 包含三个操纵:读取 counter 的值、增加 1、写回 counter 的值,这些操纵不是原子的。因此,在多线程环境中,最终的 counter 值通常会小于预期值,这证明了 volatile 变量不保证复合操纵的原子性。
办理方案

1. 使用 synchronized 方法或块:

  • 将访问 volatile 变量的方法或代码块声明为 synchronized,确保原子性和可见性。
  1. class Counter {
  2.     private volatile int count = 0;
  3.     synchronized void increment() {
  4.         count++;
  5.     }
  6.     synchronized int getCount() {
  7.         return count;
  8.     }
  9. }
复制代码
2. 使用 AtomicInteger 类:
java.util.concurrent.atomic 包中的 AtomicInteger 提供了原子操纵,可以替代 volatile 变量。
  1. import java.util.concurrent.atomic.AtomicInteger;
  2. class Counter {
  3.     private AtomicInteger count = new AtomicInteger(0);
  4.     void increment() {
  5.         count.incrementAndGet();
  6.     }
  7.     int getCount() {
  8.         return count.get();
  9.     }
  10. }
复制代码
3. 使用锁(如 ReentrantLock):
使用显式锁(如 ReentrantLock)来同步访问 volatile 变量的代码块。
  1. import java.util.concurrent.locks.Lock;
  2. import java.util.concurrent.locks.ReentrantLock;
  3. class Counter {
  4.     private volatile int count = 0;
  5.     private final Lock lock = new ReentrantLock();
  6.     void increment() {
  7.         lock.lock();
  8.         try {
  9.             count++;
  10.         } finally {
  11.             lock.unlock();
  12.         }
  13.     }
  14.     int getCount() {
  15.         lock.lock();
  16.         try {
  17.             return count;
  18.         } finally {
  19.             lock.unlock();
  20.         }
  21.     }
  22. }
复制代码
使用volatile变量的正确使用场景

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

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

天空闲话

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表