Java: 将所有字段设置为 final 还是 volatile?
Java: Make all fields either final or volatile?
如果我有一个在线程之间共享的对象,在我看来每个字段都应该是 final
或 volatile
,推理如下:
如果字段应该改变(指向另一个对象,更新原始值),那么字段应该是volatile
以便所有其他线程对新值进行操作。仅同步访问所述字段的方法是不够的,因为它们可能 return 缓存值。
如果该字段永不更改,则设为 final
。
但是,我找不到任何相关信息,所以我想知道这个逻辑是否有缺陷或太明显了?
EDIT 当然可以使用 final AtomicReference
或类似的而不是 volatile。
EDIT 例如,参见
编辑以避免混淆:这个问题是关于缓存失效的!如果两个线程操作同一个对象,如果对象未声明为易变的,则可以(每个线程)缓存对象。如何保证缓存正确失效?
最终编辑 感谢@Peter Lawrey 指出 JLS §17(Java 内存模型)。据我所知,它指出同步在操作之间建立了先行发生关系,因此如果这些更新 "happened-before",则一个线程会看到来自另一个线程的更新,例如如果非易失性字段的 getter 和 setter 是 synchronized
.
创建不需要更改的字段 final
是个好主意,与线程问题无关。它使 class 的实例更容易推理,因为您可以更容易地知道它处于什么状态。
在制作其他字段方面volatile
:
Merely a synchronization on the methods which access said field is insufficient because they might return a cached value.
如果您访问同步块之外的值,您只会看到缓存的值。
所有访问都需要正确同步。保证一个同步块的结束发生在另一个同步块的开始之前(在同一监视器上同步时)。
至少在某些情况下您仍需要使用同步:
- 如果您必须以原子方式读取然后更新一个或多个字段,您会希望使用同步。
- 您或许可以避免某些单个字段更新的同步,例如如果您可以使用
Atomic*
class 而不是 "plain old field";但即使对于单个字段更新,您仍然可能需要独占访问权限(例如,将一个元素添加到列表中同时删除另一个元素)。
- 此外,volatile/final 可能不足以用于非线程安全值,例如
ArrayList
或数组。
虽然我觉得 private final
可能应该是字段和变量的默认值,并且带有关键字 var
使其可变,但在不需要时使用 volatile 是
- 慢很多,通常慢 10 倍左右。
- 通常不会为您提供所需的线程安全性,但可以通过降低此类错误出现的可能性来增加查找此类错误的难度。
- 与
final
不同,它通过说明不应该更改来提高清晰度,在不需要时使用 volatile
可能会造成混淆,因为 reader 试图工作为什么它变得不稳定。
if the field should be changed (point to another object, update the primitive value), then the field should be volatile so that all other threads operate on the new value.
虽然这对于读取来说没问题,但请考虑一下这种微不足道的情况。
volatile int x;
x++;
这不是线程安全的。因为它和
一样
int x2 = x;
x2 = x2 + 1; // multiple threads could be executing on the same value at this point.
x = x2;
更糟糕的是,使用 volatile
会使这种错误更难找到。
正如 yshavit 指出的那样,更新多个字段更难解决 volatile
例如HashMap.put(a, b)
更新多个引用。
Merely a synchronization on the methods which access said field is insufficient because they might return a cached value.
synchronized 为您提供 volatile
甚至更多的所有内存保证,这就是它明显变慢的原因。
注意:仅 synchronized
-ing 每种方法并不总是足够的。 StringBuffer
同步了所有方法,但在多线程上下文中最糟糕的是无用,因为它的使用很可能容易出错。
人们太容易认为实现线程安全就像撒上仙尘,添加一些神奇的线程安全,您的 bug 就会消失。问题是线程安全更像是一个有很多洞的桶。堵上最大的漏洞,错误似乎就消失了,但除非你全部堵上,否则你没有线程安全,但它可能更难找到。
就同步与易失性而言,这表明
Other mechanisms, such as reads and writes of volatile variables and the use of classes in the java.util.concurrent package, provide alternative ways of synchronization.
https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html
如果对象在线程之间共享,您有两个明确的选择:
1.将该对象设为只读
因此,更新(或缓存)没有影响。
2。在对象本身上同步
缓存失效很难。 Very hard. 因此,如果您需要保证没有陈旧的值,您应该保护该值 并保护该值周围的锁 。
在共享对象上将锁和值设为私有,因此这里的操作是一个实现细节。
为了避免死锁,这个操作应该像"atomic"一样,以避免与其他任何锁交互。
如果我有一个在线程之间共享的对象,在我看来每个字段都应该是 final
或 volatile
,推理如下:
如果字段应该改变(指向另一个对象,更新原始值),那么字段应该是
volatile
以便所有其他线程对新值进行操作。仅同步访问所述字段的方法是不够的,因为它们可能 return 缓存值。如果该字段永不更改,则设为
final
。
但是,我找不到任何相关信息,所以我想知道这个逻辑是否有缺陷或太明显了?
EDIT 当然可以使用 final AtomicReference
或类似的而不是 volatile。
EDIT 例如,参见
编辑以避免混淆:这个问题是关于缓存失效的!如果两个线程操作同一个对象,如果对象未声明为易变的,则可以(每个线程)缓存对象。如何保证缓存正确失效?
最终编辑 感谢@Peter Lawrey 指出 JLS §17(Java 内存模型)。据我所知,它指出同步在操作之间建立了先行发生关系,因此如果这些更新 "happened-before",则一个线程会看到来自另一个线程的更新,例如如果非易失性字段的 getter 和 setter 是 synchronized
.
创建不需要更改的字段 final
是个好主意,与线程问题无关。它使 class 的实例更容易推理,因为您可以更容易地知道它处于什么状态。
在制作其他字段方面volatile
:
Merely a synchronization on the methods which access said field is insufficient because they might return a cached value.
如果您访问同步块之外的值,您只会看到缓存的值。
所有访问都需要正确同步。保证一个同步块的结束发生在另一个同步块的开始之前(在同一监视器上同步时)。
至少在某些情况下您仍需要使用同步:
- 如果您必须以原子方式读取然后更新一个或多个字段,您会希望使用同步。
- 您或许可以避免某些单个字段更新的同步,例如如果您可以使用
Atomic*
class 而不是 "plain old field";但即使对于单个字段更新,您仍然可能需要独占访问权限(例如,将一个元素添加到列表中同时删除另一个元素)。
- 您或许可以避免某些单个字段更新的同步,例如如果您可以使用
- 此外,volatile/final 可能不足以用于非线程安全值,例如
ArrayList
或数组。
虽然我觉得 private final
可能应该是字段和变量的默认值,并且带有关键字 var
使其可变,但在不需要时使用 volatile 是
- 慢很多,通常慢 10 倍左右。
- 通常不会为您提供所需的线程安全性,但可以通过降低此类错误出现的可能性来增加查找此类错误的难度。
- 与
final
不同,它通过说明不应该更改来提高清晰度,在不需要时使用volatile
可能会造成混淆,因为 reader 试图工作为什么它变得不稳定。
if the field should be changed (point to another object, update the primitive value), then the field should be volatile so that all other threads operate on the new value.
虽然这对于读取来说没问题,但请考虑一下这种微不足道的情况。
volatile int x;
x++;
这不是线程安全的。因为它和
一样int x2 = x;
x2 = x2 + 1; // multiple threads could be executing on the same value at this point.
x = x2;
更糟糕的是,使用 volatile
会使这种错误更难找到。
正如 yshavit 指出的那样,更新多个字段更难解决 volatile
例如HashMap.put(a, b)
更新多个引用。
Merely a synchronization on the methods which access said field is insufficient because they might return a cached value.
synchronized 为您提供 volatile
甚至更多的所有内存保证,这就是它明显变慢的原因。
注意:仅 synchronized
-ing 每种方法并不总是足够的。 StringBuffer
同步了所有方法,但在多线程上下文中最糟糕的是无用,因为它的使用很可能容易出错。
人们太容易认为实现线程安全就像撒上仙尘,添加一些神奇的线程安全,您的 bug 就会消失。问题是线程安全更像是一个有很多洞的桶。堵上最大的漏洞,错误似乎就消失了,但除非你全部堵上,否则你没有线程安全,但它可能更难找到。
就同步与易失性而言,这表明
Other mechanisms, such as reads and writes of volatile variables and the use of classes in the java.util.concurrent package, provide alternative ways of synchronization.
https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html
如果对象在线程之间共享,您有两个明确的选择:
1.将该对象设为只读
因此,更新(或缓存)没有影响。
2。在对象本身上同步
缓存失效很难。 Very hard. 因此,如果您需要保证没有陈旧的值,您应该保护该值 并保护该值周围的锁 。
在共享对象上将锁和值设为私有,因此这里的操作是一个实现细节。
为了避免死锁,这个操作应该像"atomic"一样,以避免与其他任何锁交互。