Java 示例:此代码是线程安全的吗?

Java 示例:此代码是线程安全的吗?,java,multithreading,volatile,Java,Multithreading,Volatile,我试图用一个例子来说明volatile的用法和重要性,如果省略volatile,这个例子将不会产生好的结果 但我不太习惯使用volatile。下面代码的思想是,如果省略了volatile,则会导致无限循环,如果存在volatile,则是完全线程安全的。以下代码是线程安全的吗?您是否有其他使用volatile并在没有它的情况下给出明显不正确结果的现实且简短的代码示例 代码如下: public class VolatileTest implements Runnable { private

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

更新我的答案是错误的,请参阅无法更改的答案


它不是线程安全的,因为访问
count
不是线程安全的,因为只有一个writer线程。如果存在另一个writer线程,
count
的值将与更新的数量不一致


通过检查
stopped
volatile-inside
getCount
方法,可以确保
count
值对主线程的可见性。这就是《实践中的并发》(Concurrency in practice)一书中所称的同步上的
背驮

为了说明
volatile
关键字在并发方面的重要性,您所需要做的就是确保在单独的线程中修改并读取
volatile
字段。

我一直很难用令人信服的方式来说明并发问题:好吧,好吧,关于之前发生的事情和其他事情都很好,但为什么要在意呢?真的有问题吗?有许许多多写得不好、同步性差的程序,它们大部分时间都在工作

我过去常常在“大部分时间有效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
也就是说,一个正在运行的线程需要超过10秒(这里是15秒)才能注意到有任何更改

使用
volatile
,您可以:

Waiting  1319229288280
stopping 1319229298281
stopped  1319229298281
Delay: 0
也就是说,立即(几乎)退出。
currentTimeMillis
的分辨率约为10ms,因此差异超过1000倍

请注意,这是苹果的(ex-)Sun JDK版本,带有
-server
选项。添加10秒等待是为了让JIT编译器发现循环足够热,并对其进行优化


希望这会有所帮助。

维克托是对的,您的代码存在一些问题:原子性和可见性

这是我的版本:

    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
,则保证工作完成并且结果可见

从volatile write到volatile read(在同一个变量上)有一个before关系,因此如果有两个线程

   thread 1              thread 2

   action A
       |
 volatile write  
                  \
                     volatile read
                          |  
                       action B

动作A发生在动作B之前;A中的写入可以被B看到。

错误的代码,如果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.
    }
  }
}

来源:

进一步简化@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();
  }
}

根据JLS,int更新是原子的。但是,如果没有volatile,当变量没有被同步隔离时,读/写顺序可能会有问题。getCount中异常的想法是确保只有在对count的写入完成后,外部线程才能访问count。我认为写入volatile变量可以保证count对所有线程的可见性。是吗?@JBNizet,为什么你认为在一个线程中修改一个字段并期望在不同步的情况下从另一个线程读取该字段时获得正确的值是可以的?更新可能是,但代码使用的是++,这是读取更改存储操作。谢谢大家的回答。我知道我会错过一些东西。我很高兴我不是唯一一个:-)事实上,这个例子是错误的,因为
count++
不是一个原子操作。您必须获取该操作周围的锁,或者使用非阻塞的
AtomicInteger
(当然,除非您确保只有一个线程将调用方法
run()
——无论如何,上面的代码不能声明为线程安全的)