为什么代码会有风险看到部分构造的对象?

时间:2016-01-13 03:30:37

标签: java multithreading concurrency volatile

在ibm中有关于volatile使用的article,并且解释让我困惑,下面是本文的示例及其解释:

public class BackgroundFloobleLoader {
    public volatile Flooble theFlooble;

    public void initInBackground() {
        // do lots of stuff
        theFlooble = new Flooble();  // this is the only write to theFlooble
    }
}

public class SomeOtherClass {
    public void doWork() {
        while (true) { 
            // do some stuff...
            // use the Flooble, but only if it is ready
            if (floobleLoader.theFlooble != null) 
                doSomething(floobleLoader.theFlooble);
        }
    }
}
  

如果theFlooble引用不是volatile,那么doWork()中的代码将有可能看到部分构造的Flooble,因为它取消引用了Flaloble引用。

如何理解这一点?为什么没有volatile,我们可能会使用部分构造的Flooble对象?谢谢!

5 个答案:

答案 0 :(得分:2)

如果没有volatile,您可以看到部分构造的对象。例如。考虑这个Flooble对象。

public class Flooble {
    public int x;
    public int y;

    public Flooble() {
       x = 5;
       y = 1;
    }
}

public class SomeOtherClass {
    public void doWork() {
    while (true) { 
        // do some stuff...
        // use the Flooble, but only if it is ready
        if (floobleLoader.theFlooble != null) 
            doSomething(floobleLoader.theFlooble);
    }

    public void doSomething(Flooble flooble) {
        System.out.println(flooble.x / flooble.y);
    }
}

}

如果没有volatile方法,则无法保证doSomething会看到51的值xy。它可以看到例如x == 5y == 0,导致除以零。

执行此操作theFlooble = new Flooble()时,会发生三次写入:

  1. tmpFlooble.x = 5
  2. tmpFlooble.y = 1
  3. theFlooble = tmpFlooble
  4. 如果这些写入按此顺序发生,一切正常。但是如果没有volatile,编译器可以自由地重新排序这些写入并按照自己的意愿执行它们。例如。第一点3,然后点1和2.

    这实际上一直都在发生。编译器确实对写入进行了重新排序。这样做是为了提高性能。

    错误很容易发生在以下方面:

    线程A从类initInBackground()执行BackgroundFloobleLoader方法。编译器在执行Flooble()(正在设置xy的主体)之前重新排序写入,线程A首先执行theFlooble = new Flooble()。现在,theFlooble指向一个flooble实例,其xy0。在线程A继续之前,其他一些线程B执行类doWork()的方法SomeOtherClass。此方法使用当前值doSomething(floobleLoader.theFlooble)调用方法theFlooble。在此方法中,theFlooble.x除以theFlooble.y,导致除以零。由于未捕获的异常,线程B完成。线程A继续并设置theFlooble.x = 5theFlooble.y = 1

    这种情况当然不会在每次运行时发生,但根据Java规则,可以发生。

答案 1 :(得分:0)

当不同的线程访问您的代码时,任何线程都可以对对象的状态执行修改,这意味着当其他线程访问它时,状态可能不是它应该的。

来自oracle文档:

  

Java编程语言允许线程访问共享   变量。通常,确保共享变量   一致且可靠地更新,线程应确保它具有   通过获得锁定来独占使用这些变量,   通常,对这些共享变量实施互斥。

     

Java编程语言提供了第二种机制,即volatile   字段,这比某些目的的锁定更方便。

     

字段可以声明为volatile,在这种情况下是Java Memory Model   确保所有线程都能看到变量的一致值。

source

这意味着此变量的值永远不会在线程本地缓存,所有读取和写入将直接进入"主内存"

例如图片thread1和thread2访问对象:

  1. Thread1访问该对象并将其存储在本地缓存中
  2. Trhead2修改对象
  3. Thread1再次访问该对象,但由于它仍然在其缓存中,因此它不会通过thread2访问更新后的状态。

答案 2 :(得分:0)

从执行此操作的代码的角度来看:

        if (floobleLoader.theFlooble != null) 
            doSomething(floobleLoader.theFlooble);

显然,您需要保证在new Flooble()可能测试为theFlooble之前,!= null执行的所有写操作都对此代码可见。没有volatile的代码中没有任何内容提供此保证。所以你需要一个你没有的保证。失败。

Java提供了几种获得所需保证的方法。一种是使用volatile变量:

  

...对volatile变量的任何写入都会建立与之后读取同一变量的先发生关系。这意味着对volatile变量的更改始终对其他线程可见。更重要的是,它还意味着当线程读取volatile变量时,它不仅会看到volatile的最新更改,还会看到导致更改的代码的副作用。 - Docs

因此,在一个线程中对volatile进行写入,在另一个线程中对volatile进行读取,准确地确定了我们需要的事先关系。

答案 3 :(得分:-2)

我怀疑在Java中存在部分构造的对象这样的事情。易失性保证每个线程都能看到构造的对象。由于volatile就像引用对象上的一个微小同步块一样,如果theFlobble == null,你最终会得到一个NPE。也许这就是他们的意思。

答案 4 :(得分:-2)

对象封装了很多东西:变量,方法等等,这些需要时间才能在计算机中存在。在Java中,如果任何变量被声明为volatile,则对它的所有读取和写入都是原子的。因此,如果引用对象的变量被声明为volatile,则只有在系统中完全加载时才允许访问其成员(如何读取或写入那些根本不存在的内容?)