为什么代码会面临看到部分构造的对象的风险?
Why the code would be at risk for seeing a partially constructed object?
关于在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);
}
}
}
Without the theFlooble reference being volatile, the code in doWork() would be at risk for seeing a partially constructed Flooble as it dereferences the theFlooble reference.
这个怎么理解?为什么没有volatile,我们可以使用部分构造的Flooble
对象?谢谢!
当不同的线程访问你的代码时,任何线程都可以对你的对象的状态进行修改,这意味着当其他线程访问它时,状态可能不是它应该的状态。
来自 oracle 文档:
The Java programming language allows threads to access shared
variables. As a rule, to ensure that shared variables are
consistently and reliably updated, a thread should ensure that it has
exclusive use of such variables by obtaining a lock that,
conventionally, enforces mutual exclusion for those shared variables.
The Java programming language provides a second mechanism, volatile
fields, that is more convenient than locking for some purposes.
A field may be declared volatile, in which case the Java Memory Model
ensures that all threads see a consistent value for the variable.
也就是说这个变量的值永远不会被缓存thread-locally,所有的读写都会直接到"main memory"
例如图片thread1和thread2访问对象:
- Thread1 访问对象并将其存储在其本地缓存中
- Trhead2 修改对象
- 线程 1 再次访问该对象,但由于它仍在其缓存中,因此它不会访问线程 2 更新后的状态。
我怀疑 Java 中是否存在部分构造的对象。 Volatile 保证每个线程都会看到一个构造的对象。由于 volatile 在引用的对象上像一个微小的同步块一样工作,如果 theFlobble == null
,你最终会得到一个 NPE。也许这就是他们的意思。
对象封装了很多东西:变量、方法等,这些都需要时间才能在计算机中出现。在 Java 中,如果任何变量被声明为 volatile,那么所有对它的读写都是原子的。因此,如果引用对象的变量被声明为 volatile,则只有当它完全加载到您的系统中时才允许访问其成员(您如何读取或写入根本不存在的东西?)
如果没有 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 方法不能保证看到 x
和 y
的值 5
和 1
。例如,它可以看到 x == 5
但 y == 0
,导致被零除。
执行此操作时theFlooble = new Flooble()
,发生3次写入:
tmpFlooble.x = 5
tmpFlooble.y = 1
theFlooble = tmpFlooble
如果这些写入按此顺序发生,则一切正常。但是如果没有 volatile
,编译器可以自由地重新排序这些写入并按需要执行它们。例如。首先是第 3 点,然后是第 1 点和第 2 点。
这实际上一直都在发生。编译器确实会重新排序写入。这样做是为了提高性能。
错误很容易通过以下方式发生:
线程 A
从 class BackgroundFloobleLoader
执行 initInBackground()
方法。编译器重新排序写入,因此在执行 Flooble()
的主体(其中设置了 x
和 y
)之前,线程 A
首先执行 theFlooble = new Flooble()
。现在,theFlooble
指向一个 flooble 实例,其 x
和 y
是 0
。在线程 A
继续之前,其他线程 B
执行了 class SomeOtherClass
的方法 doWork()
。此方法使用 theFlooble
的当前值调用方法 doSomething(floobleLoader.theFlooble)
。在此方法中,theFlooble.x
除以 theFlooble.y
,结果除以零。线程 B
由于未捕获的异常而结束。线程 A
继续并设置 theFlooble.x = 5
和 theFlooble.y = 1
.
这种情况当然不会发生在每个运行,但是根据Java的规则,可以发生。
从执行此操作的代码的角度来看:
if (floobleLoader.theFlooble != null)
doSomething(floobleLoader.theFlooble);
显然,在 theFlooble
可能测试为 != null
之前,您需要保证 new Flooble()
执行的所有写入对该代码可见。没有 volatile
的代码中没有任何内容提供这种保证。所以你需要一个你没有的保证。失败。
Java 提供了几种方法来获得您需要的保证。一种是使用 volatile
变量:
... any write to a volatile variable establishes a happens-before relationship with subsequent reads of that same variable. This means that changes to a volatile variable are always visible to other threads. What's more, it also means that when a thread reads a volatile variable, it sees not just the latest change to the volatile, but also the side effects of the code that led up the change. -- Docs
因此,在一个线程中写入 volatile
并在另一个线程中读取 volatile
恰好建立了我们需要的 happens-before 关系。
关于在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);
}
}
}
Without the theFlooble reference being volatile, the code in doWork() would be at risk for seeing a partially constructed Flooble as it dereferences the theFlooble reference.
这个怎么理解?为什么没有volatile,我们可以使用部分构造的Flooble
对象?谢谢!
当不同的线程访问你的代码时,任何线程都可以对你的对象的状态进行修改,这意味着当其他线程访问它时,状态可能不是它应该的状态。
来自 oracle 文档:
The Java programming language allows threads to access shared variables. As a rule, to ensure that shared variables are consistently and reliably updated, a thread should ensure that it has exclusive use of such variables by obtaining a lock that, conventionally, enforces mutual exclusion for those shared variables.
The Java programming language provides a second mechanism, volatile fields, that is more convenient than locking for some purposes.
A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable.
也就是说这个变量的值永远不会被缓存thread-locally,所有的读写都会直接到"main memory"
例如图片thread1和thread2访问对象:
- Thread1 访问对象并将其存储在其本地缓存中
- Trhead2 修改对象
- 线程 1 再次访问该对象,但由于它仍在其缓存中,因此它不会访问线程 2 更新后的状态。
我怀疑 Java 中是否存在部分构造的对象。 Volatile 保证每个线程都会看到一个构造的对象。由于 volatile 在引用的对象上像一个微小的同步块一样工作,如果 theFlobble == null
,你最终会得到一个 NPE。也许这就是他们的意思。
对象封装了很多东西:变量、方法等,这些都需要时间才能在计算机中出现。在 Java 中,如果任何变量被声明为 volatile,那么所有对它的读写都是原子的。因此,如果引用对象的变量被声明为 volatile,则只有当它完全加载到您的系统中时才允许访问其成员(您如何读取或写入根本不存在的东西?)
如果没有 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 方法不能保证看到 x
和 y
的值 5
和 1
。例如,它可以看到 x == 5
但 y == 0
,导致被零除。
执行此操作时theFlooble = new Flooble()
,发生3次写入:
tmpFlooble.x = 5
tmpFlooble.y = 1
theFlooble = tmpFlooble
如果这些写入按此顺序发生,则一切正常。但是如果没有 volatile
,编译器可以自由地重新排序这些写入并按需要执行它们。例如。首先是第 3 点,然后是第 1 点和第 2 点。
这实际上一直都在发生。编译器确实会重新排序写入。这样做是为了提高性能。
错误很容易通过以下方式发生:
线程 A
从 class BackgroundFloobleLoader
执行 initInBackground()
方法。编译器重新排序写入,因此在执行 Flooble()
的主体(其中设置了 x
和 y
)之前,线程 A
首先执行 theFlooble = new Flooble()
。现在,theFlooble
指向一个 flooble 实例,其 x
和 y
是 0
。在线程 A
继续之前,其他线程 B
执行了 class SomeOtherClass
的方法 doWork()
。此方法使用 theFlooble
的当前值调用方法 doSomething(floobleLoader.theFlooble)
。在此方法中,theFlooble.x
除以 theFlooble.y
,结果除以零。线程 B
由于未捕获的异常而结束。线程 A
继续并设置 theFlooble.x = 5
和 theFlooble.y = 1
.
这种情况当然不会发生在每个运行,但是根据Java的规则,可以发生。
从执行此操作的代码的角度来看:
if (floobleLoader.theFlooble != null)
doSomething(floobleLoader.theFlooble);
显然,在 theFlooble
可能测试为 != null
之前,您需要保证 new Flooble()
执行的所有写入对该代码可见。没有 volatile
的代码中没有任何内容提供这种保证。所以你需要一个你没有的保证。失败。
Java 提供了几种方法来获得您需要的保证。一种是使用 volatile
变量:
... any write to a volatile variable establishes a happens-before relationship with subsequent reads of that same variable. This means that changes to a volatile variable are always visible to other threads. What's more, it also means that when a thread reads a volatile variable, it sees not just the latest change to the volatile, but also the side effects of the code that led up the change. -- Docs
因此,在一个线程中写入 volatile
并在另一个线程中读取 volatile
恰好建立了我们需要的 happens-before 关系。