问题描述
我正在对处理多线程 Java 应用程序中的竞争条件的不同方法进行实验。诸如原子变量、同步之类的策略效果很好,但我认为使用 volatile 变量时没有解决问题。这里的代码和输出供参考。
您能否就 volatile 变量仍然导致竞争条件的原因进行指导?
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("normal counter(Race condition) : " + dm1.getCounter() );
System.out.println("Synchronized counter is :" + dm1.getSyncCounter());
System.out.println("Atomic counter is :" + dm1.getAtomicCounter());
System.out.println("Volatile counter is :" + dm1.getVolatileCounter());
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++;
}
}
这是我得到的输出:
normal counter(Race condition) : 197971
Synchronized counter is :200000
Atomic counter is :200000
Volatile counter is :199601
解决方法
可见性与原子性
volatile
只解决了可见性的问题。这意味着每个线程都会看到变量的当前值,而不是可能看到过时的缓存值。
您的线路:
this.volatileCounter++;
…正在执行多个操作:
- 获取该变量的当前值
- 增加该值
- 将新值存储在该变量中
这组操作不是原子的。
当一个线程已获取该值但尚未递增并存储新值时,第二个线程可能会访问相同的当前值。两个线程都增加相同的初始值,因此两个线程产生并保存相同的冗余新值。
例如,两个或多个线程可能访问值 42。然后所有这些线程将增加到 43,每个线程将存储 43。这个数字 43 将一遍又一遍地存储。其他一些线程甚至可能已经看到了这 43 次写入中的一个,然后递增并存储了 44。尚未写入其 43 的剩余线程之一将最终覆盖 44 的写入。因此,您不仅可能会浪费一些增加的尝试从而无法向前移动数字,您实际上可能会看到数字向后移动(实际上是递减)。
如果您想使用 volatile
,您必须保护代码以使多个操作原子化。 synchronized
关键字就是这样一种解决方案。
就我个人而言,我更喜欢使用 AtomicInteger
方法。如果您在任何访问尝试之前实例化 AtomicInteger
,并且从不替换该实例,则该 AtomicInteger
的引用变量的可见性不是问题。没有机会获得陈旧的缓存值意味着没有可见性问题。关于对其有效载荷的无情访问,AtomicInteger
的方法为简单操作提供了原子性(显然,因此得名),
要详细了解可见性问题,请研究 Java Memory Model。并阅读 Brian Goetz 等人撰写的优秀书籍 Java Concurrency In Practice。