我试图用一个例子来说明volatile
的用法和重要性,如果省略volatile
则该例子真的不会给出好的结果。
但我并不习惯使用volatile
。如果省略volatile
,则以下代码的想法是导致无限循环,并且如果存在volatile
则完全是线程安全的。以下代码是否是线程安全的?您是否有任何其他使用volatile
的现实和简短的代码示例,如果没有它会产生明显不正确的结果?
以下是代码:
public class VolatileTest implements Runnable {
private int count;
private volatile boolean stopped;
@Override
public void run() {
while (!stopped) {
count++;
}
System.out.println("Count 1 = " + count);
}
public void stopCounting() {
stopped = true;
}
public int getCount() {
if (!stopped) {
throw new IllegalStateException("not stopped yet.");
}
return count;
}
public static void main(String[] args) throws InterruptedException {
VolatileTest vt = new VolatileTest();
Thread t = new Thread(vt);
t.start();
Thread.sleep(1000L);
vt.stopCounting();
System.out.println("Count 2 = " + vt.getCount());
}
}
答案 0 :(得分:10)
Victor是对的,您的代码存在问题:原子性和可见性。
这是我的版本:
private int count;
private volatile boolean stop;
private volatile boolean stopped;
@Override
public void run() {
while (!stop) {
count++; // the work
}
stopped = true;
System.out.println("Count 1 = " + count);
}
public void stopCounting() {
stop = true;
while(!stopped)
; //busy wait; ok in this example
}
public int getCount() {
if (!stopped) {
throw new IllegalStateException("not stopped yet.");
}
return count;
}
}
如果某个帖子观察到stopped==true
,则可以保证工作完成并且结果可见。
从易失性写入到易失性读取(在同一个变量上)之前存在一个发生在之前的关系,所以如果有两个线程
thread 1 thread 2
action A
|
volatile write
\
volatile read
|
action B
行动A发生在行动B之前; B中可以看到A中的写入。
答案 1 :(得分:1)
我总是很难用令人信服的方式来说明并发问题:好吧,很好,发生在和之前的事情都很好,但为什么要关心?有真正的问题吗?有很多很多写得不好,程序不合理的程序 - 而且它们大部分时间都在工作。
我曾经在“大部分时间工作 VS 工作”修辞中找到一个度假胜地 - 但是,坦率地说,这是一种微弱的方法。因此,我需要的是一个能够明显区别的例子 - 最好是痛苦的。
所以这是一个确实显示差异的版本:
public class VolatileExample implements Runnable {
public static boolean flag = true; // do not try this at home
public void run() {
long i = 0;
while (flag) {
if (i++ % 10000000000L == 0)
System.out.println("Waiting " + System.currentTimeMillis());
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new VolatileExample());
thread.start();
Thread.sleep(10000L);
flag = false;
long start = System.currentTimeMillis();
System.out.println("stopping " + start);
thread.join();
long end = System.currentTimeMillis();
System.out.println("stopped " + end);
System.out.println("Delay: " + ((end - start) / 1000L));
}
}
一个简单的运行显示:
Waiting 1319229217263
stopping 1319229227263
Waiting 1319229242728
stopped 1319229242728
Delay: 15
也就是说,正在运行的线程需要超过十秒(15这里)才能注意到任何更改。
使用volatile
,您有:
Waiting 1319229288280
stopping 1319229298281
stopped 1319229298281
Delay: 0
即立即退出(几乎)。 currentTimeMillis
的分辨率约为10毫秒,因此差异超过1000倍。
请注意,这是Apple的(前)Sun JDK版本,-server
选项。添加了10秒等待,以便让JIT编译器发现循环足够热,并对其进行优化。
希望有所帮助。
答案 2 :(得分:1)
进一步简化@Elf示例,其他线程永远不会获得由其他线程更新的值。删除System.out.println因为println中存在同步代码而out是静态的,以某种方式帮助其他线程获取flag变量的最新值。
public class VolatileExample implements Runnable {
public static boolean flag = true;
public void run() {
while (flag);
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new VolatileExample());
thread.start();
Thread.sleep(1000L);
flag = false;
thread.join();
}
}
答案 3 :(得分:0)
更新我的回答是错误的,请参阅无可争议的答案。
不线程安全,因为访问,只有一个编写器线程。如果有另一个编写者线程,count
不是count
的值将与更新次数不一致。
通过检查count
volatile stopped
方法,可以确保getCount
值对主线程的可见性。这就是piggybacking on synchronization
本书中所谓的Concurrency in practice
。
答案 4 :(得分:0)
为了说明volatile
关键字在并发性方面的重要性,您需要做的就是确保修改volatile
字段并在单独的线程中读取。
答案 5 :(得分:0)
如果y已经是2,那么我们不能假设x = 1的代码错误:
Class Reordering {
int x = 0, y = 0;
public void writer() {
x = 1;
y = 2;
}
public void reader() {
int r1 = y;
int r2 = x;
}
}
使用volatile关键字的示例:
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
//uses x - guaranteed to see 42.
}
}
}
来源:http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html