Java中的Volatile变量仍然导致竞争条件
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
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这本书。