说明volatile:这段代码是线程安全的吗?

时间:2011-10-21 15:00:43

标签: java multithreading volatile

我试图用一个例子来说明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());
    }
}

6 个答案:

答案 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