Java中的Volatile变量仍然导致竞争条件

29 浏览
0 Comments

Java中的Volatile变量仍然导致竞争条件

我正在运行一个关于处理多线程Java应用程序中的竞态条件的实验。像原子变量、同步等策略效果很好,但是我发现使用volatile变量时问题没有得到解决。以下是代码和输出供参考。\n请指导一下,为什么volatile变量仍然会导致竞态条件?\n

package com.shyam.concurrency;
public class main {
    public static void main(String[] args) {
        demoClass dm1 = new demoClass();
        Thread th1 = new Thread(() -> {
            int i = 0;
            do {
                i++;
                dm1.setCounter();
                dm1.setAtomicCounter();
                dm1.setSyncCounter();
                dm1.setVolatileCounter();
            } while (i < 100000);
        });
        Thread th2 = new Thread(() -> {
            int i = 0;
            do {
                i++;
                dm1.setCounter();
                dm1.setAtomicCounter();
                dm1.setSyncCounter();
                dm1.setVolatileCounter();
            } while (i < 100000);
        });
        th1.start();
        th2.start();
        try {
            th1.join();
            th2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("普通计数器(竞态条件):" + dm1.getCounter());
        System.out.println("同步计数器:" + dm1.getSyncCounter());
        System.out.println("原子计数器:" + dm1.getAtomicCounter());
        System.out.println("Volatile计数器:" + dm1.getVolatileCounter());
    }
}

\n具有递增逻辑的代码如下:\n

package com.shyam.concurrency;
import java.util.concurrent.atomic.AtomicInteger;
public class demoClass {
    private int counter;
    private int syncCounter;
    private volatile int volatileCounter = 0;
    private AtomicInteger atomicCounter = new AtomicInteger();
    public int getAtomicCounter() {
        return atomicCounter.intValue();
    }
    public void setAtomicCounter() {
        this.atomicCounter.addAndGet(1);
    }
    public int getCounter() {
        return counter;
    }
    public void setCounter() {
        this.counter++;
    }
    public synchronized int getSyncCounter() {
        return syncCounter;
    }
    public synchronized void setSyncCounter() {
        this.syncCounter++;
    }
    public int getVolatileCounter() {
        return volatileCounter;
    }
    public void setVolatileCounter() {
        this.volatileCounter++;
    }
}

\n以下是输出结果:\n

普通计数器(竞态条件):197971
同步计数器:200000
原子计数器:200000
Volatile计数器:199601

0
0 Comments

Java中的Volatile变量仍然会导致竞态条件

在Java中,Volatile关键字只解决了可见性的问题。这意味着每个线程都能看到变量的当前值,而不是可能看到一个过时的缓存值。

你的代码中的一行:

this.volatileCounter++;

执行了多个操作:

- 获取变量的当前值

- 增加该值

- 将新值存储在变量中

这组操作不是原子操作

当一个线程获取了值但尚未增加和存储新值时,第二个线程可能访问相同的当前值。两个线程都对相同的初始值进行增加操作,因此两个线程产生并保存了相同的冗余新值。

例如,两个或多个线程可能访问值为42的变量。所有这些线程都会将其增加到43,并且每个线程都会保存43。数字43会一遍又一遍地被保存。其他某个线程甚至可能已经看到了其中一个43的写入,然后进行了增加并保存了44。尚未写入其43的剩余线程之一将覆盖写入的44。因此,您不仅可能浪费了一些增加尝试并因此未能使数字前进,还可能实际上看到数字向后移动(实际上是递减)。

如果要使用Volatile,必须保护代码以使多个操作成为原子操作。synchronized关键字是一种解决方案。

个人而言,我更喜欢使用AtomicInteger方法。如果在任何访问尝试之前实例化一个AtomicInteger,并且从不替换该实例,则该AtomicInteger的引用变量的可见性不是问题。没有过时缓存值的机会意味着没有可见性问题。至于对其有效负载的竞争性访问,AtomicInteger的方法为简单的操作提供了原子性。

要了解更多关于可见性问题的信息,请学习Java内存模型。并阅读优秀的书籍《Java并发编程实战》(Java Concurrency In Practice) by Brian Goetz等人。

我会添加一些说明。谢谢。

只有读取-修改-写入操作不能仅通过volatile读取和写入来保证原子性。但实际的加载和存储是原子的。

这个解释不完全正确。Volatile处理三个方面:可见性、原子性和排序;不仅仅是可见性。如果你做了一个写入(发布),然后稍后你将对同一volatile变量进行读取(获取),那么在发布之前的所有加载/存储都会在发布之前排序。对于获取,它将对获取之后的所有加载和存储进行排序。Volatile还保证了原子性;在这种特殊情况下,无法发生读取错误或写入错误。

Volatile关键字只解决可见性问题,而不解决原子性问题。因此,在进行多个操作时,需要使用同步关键字或者使用AtomicInteger类来保证操作的原子性。使用AtomicInteger可以避免可见性问题和竞态访问问题,因为它提供了原子的操作方法。想要了解更多关于可见性问题和Java内存模型的知识,可以参考Java Concurrency In Practice这本书。

0