16

我想了解为什么声明为 final 的引用不能声明为 Volatile。关于 SO [ Why can an Object member variable can not be both final and volatile in Java?

[1]:为什么Java中的Object成员变量不能既是final又是volatile?但我不确定该答案是否理解 FINAL。

现在 final 变量的状态在它被初始化后肯定可以改变。只有引用不能初始化到另一个对象。

例如,考虑以下成员变量

final StringBuilder sb = new StringBuilder("CAT");

现在另一个线程将 sb 更改为:

sb.append("S");

如果此变量为 Non-Volatile ,则此更改是否可用于根据 Java 内存模型的不同线程?

编辑:我将 StringBuffer 更改为 StringBuilder 以使某些人清楚我的观点。

4

7 回答 7

16

volatile 意味着该字段将发生变化。如果它是最终的,你将永远不会改变它,这样没有意义。

如果此变量为 Non-Volatile ,则此更改是否可用于根据 Java 内存模型的不同线程?

在 volatile 写入发生后,声明一个字段 volatile 对其内容或修改没有影响。如果该字段是易失性或非易失性的,则其记忆效应append由 StringBuffer 的实现决定。

因此,在 StringBuffer 的情况下,它确实确保了内存可见性,但不是出于您认为的原因。StringBuffer 是同步的(线程安全的),因此您将始终拥有内容的最新值。

另一方面,StringBuilder 不同步,并且不能保证内存可见性。因此,如果您尝试将两者换成多线程测试,您会看到不同的结果。

于 2013-09-11T18:41:18.143 回答
9

Because volatile affects the behavior of how Threads access and edit the variable. In your code example, the variable is a reference (sb) to an object. So this means volatile has no effect on the object. final locks the reference. So if you append text, you are not changing the reference. Thus, it makes no sense to use volatile and final at the same time.

Will this change be available to different threads as per Java memory model if this variable is Non-Volatile ?

Because you are using a non thread safe implementation by using StringBuilder, there is no guarantee that all Threads are having the latest state of the StringBuilder.

于 2013-09-11T18:40:39.960 回答
6

如果此变量为非易失性,则此更改是否可用于根据 Java 内存模型的不同线程

是的,但那是因为 StringBuffer 是线程安全的——这意味着它在内部提供锁定,这将导致内存屏障,因此其他线程可以看到更新。

可变的引用不会影响对对象的操作,它会影响引用。

所以你可以做

 volatile StringBuilder b = new StringBuilder();

 b = someOtherStringBuilder;

现在由于 b 是可变的,其他线程将看到此引用的更新。

然而做

 b.append("foo");

不能保证其他线程会看到对现有b对象的更改。StringBuilder 与 StringBuffer 不同,它不是线程安全的,因此无论如何您都不应该在不提供自己的锁定的情况下这样做。

如果您想保证b.append("foo");在没有任何锁定的情况下对其他线程可见,则 StringBuilder 中的每个成员字段也需要是 volatile 的。(虽然这并不能使它成为线程安全的)

于 2013-09-11T18:45:11.403 回答
5

请记住这一点volatilefinal影响变量/成员,但影响其中命名/引用的对象的可变性(或操作的线程安全性)!(其他答案解释了为什么修饰符一起没有意义。)

在给定的情况下,sb.append(..) 修改命名/引用的对象(尽管它不修改变量/成员)并且不安全用于使用任一修饰符的交叉线程。


然而,正如其他人所指出的,StringBuilder 添加了一些内部同步,这仅意味着它不会在其内部数据结构级别“损坏”。

但是,切换到 StringBuilder 仍然不是[固有地] 线程安全的。线程安全与各种动作的原子范围有很大关系——并且在没有看到所有正在运行的东西以及如何允许它在有效执行中进行交互的情况下做出判断会产生误导。

例如,考虑以下从多个线程执行的代码(并假设它sb现在是一个 StringBuilder 对象)。顺序是什么?

sb.append("a").append("b");

如果代码更改如下会发生什么?[更多]“线程安全”是哪个?

synchronized (this) {
  sb.append("a").append("b");
}
于 2013-09-11T18:44:00.037 回答
3

这个问题完全误解了跨多个线程的对象引用的含义。一般来说,volatile 最有趣的是原语,这通常是你看到它的地方。

final 和 volatile 只影响变量,因为它包含一个对象引用,它是对象在内存中的位置的句柄,仅此而已。因此,为了说明,假设对 StringBuilder(或其他)的引用是 9AF5。如果那是最终的,则在构造对象后它不会改变。因此它是线程安全的(至少从 Java 1.5 开始,在此之前不记得了),因为任何线程都可以引用该变量,引用该变量的副本,或者 CPU 想要做的任何事情来优化该访问,因为最后当天,它不会改变,因此,如果您查看 9AF5,您不必检查包含在具有最终声明的对象中的引用的值是否真的改变了。

由于 final 将线程安全性保证到与 volatile 相同的级别,因此 volatile 是多余的。

如果变量是非最终的,那么 volatile 确保所有线程一致地看到 9AF5,并且如果您更改值(例如通过分配新的 StringBuilder),因此该值现在变为 1D7C,则没有线程将坚持无效的旧副本并认为该值仍然是 9AF5。

这些都不会影响 StringBuilder 对象的内容,只会影响其在内存中的位置的句柄。

于 2013-09-11T18:57:36.360 回答
2

volatile提供的内存可见性保证final实际上非常相似:

  • volatile保证线程 A 在写入volatile变量之前所做的任何更改将在随后从该变量读取之后被线程 B 看到

  • finalfinal如果线程 B 通过该字段访问该对象,则保证线程 A 在该字段初始化之前对该字段所引用的对象的状态所做的任何更改都将被线程 B 看到

如您所见,鉴于finalfield 通常只能初始化一次,这些保证非常相似 - 唯一的主要区别是finalfield 提供的保证仅涵盖该字段引用的对象。

因此,将这些保证组合在同一个字段上没有多大意义。

此外,当引用对象的状态发生更改而不更改字段本身时,这两种保证都不涵盖相关情况。

于 2013-09-11T19:04:03.890 回答
0

当您将对象字段声明为 final 时,您需要在对象的构造函数中对其进行初始化,然后 final 字段不会改变它的值。当您将对象的字段声明为 volatile 时,字段的值可能会发生变化,但从任何线程读取的值仍然会看到写入它的最新值。

所以,一个 volatile 字段可以保证当你改变它时会发生什么。(没有它可能引用的对象)。最终字段不能碰巧(可以更改字段引用的内容)。两者兼有是没有意义的。

于 2013-09-11T18:45:52.093 回答