应该同步 getter 和 setter 吗?

private double value;


public synchronized void setValue(double value) {
this.value = value;
}
public double getValue() {
return this.value;
}

在上面的例子中,让 getter 同步有什么意义吗?

36070 次浏览

我认为最好在这里引用 实践中的 Java 并发:

假设只有在写入共享变量时才需要使用同步是一个常见的错误; 这是完全不正确的。

对于可由多个人访问的每个可变状态变量 线程,则该变量的 所有权限必须使用相同的 锁定。在这种情况下,我们说,变量被保护 锁定。

在没有同步的情况下,编译器、处理器和运行时可以按照操作的执行顺序进行一些彻头彻尾的怪异操作。试图推断在同步不充分的多线程程序中内存操作“必须”发生的顺序几乎肯定是不正确的。

通常情况下,你不需要对原语那么小心,所以如果这是 int或者 boolean,它可能是:

当一个线程在不同步的情况下读取一个变量时,它可能会看到一个 但是它至少看到一个实际放置的值 而不是某个随机值。

然而,对于64位操作来说,情况并非如此,例如在 longdouble上,如果它们没有声明为 volatile:

Java 内存模型需要获取和 存储操作为原子操作,但对于非易失性的 long 和 double 变量,则允许 JVM 将64位的读或写视为两个 单独的32位操作。如果读和写发生在不同的 线程,因此可以读取非易失性 long 并获取 返回一个值的高32位和另一个值的低32位。

因此,即使您不关心陈旧的值,使用它也是不安全的 多线程程序中共享的可变长变量和双变量 除非它们被宣布为易挥发的或者被锁保护着。

让我举个例子来说明 JIT 编译代码的合法方式,你可以这样写:

while (myBean.getValue() > 1.0) {
// perform some action
Thread.sleep(1);
}

JIT 汇编:

if (myBean.getValue() > 1.0)
while (true) {
// perform some action
Thread.sleep(1);
}

在略有不同的场景中,即使是 Java 编译器也可以产生类似的字节码(它只需要消除克劳斯·福尔曼到不同的 getValue的可能性)。这是吊装的典型例子。

为什么这是合法的?编译器有权假定在执行上述代码时,myBean.getValue()的结果永远不会改变。如果没有 synchronized,则允许忽略其他线程的任何操作。

这里的原因是防止任何其他线程在线程读取时更新值,从而避免对过期值执行任何操作。

这里 get 方法将获取“ this”上的内部锁,因此任何其他线程可能尝试使用 setter 方法设置/更新,将不得不等待获取“ this”上的锁,以进入通过执行 get 的线程已经获取的 setter 方法。

这就是为什么建议在对可变状态执行任何操作时遵循使用相同锁的实践。

因为没有复合语句,所以使字段不稳定将在这里起作用。


值得注意的是,同步方法使用内部锁,即“ this”。因此 get 和 set 都被同步意味着任何进入该方法的线程都必须获取这个锁。


当执行非原子64位操作时,应该特别考虑。从 Java 并发实践中摘录的内容可能有助于理解这种情况-

“ Java 内存模型要求获取和存储操作是原子的,但对于非易失性的长变量和双变量,允许 JVM 将64位读写操作视为两个独立的32 位操作。如果读写发生在不同的线程中,那么就有可能读取一个非易失性的 long 并返回一个值的高32位和另一个值的低32位。因此,即使您不关心陈旧的值, 在多线程程序中使用共享可变的 long 和 double 变量是不安全的,除非声明它们 易挥发的或由锁保护的

也许对某些人来说,这个代码看起来很糟糕,但是它工作得很好。

  private Double value;
public  void setValue(Double value){
updateValue(value, true);
}
public Double getValue(){
return updateValue(value, false);
}
private double updateValue(Double value,boolean set){
synchronized(MyClass.class){
if(set)
this.value = value;
return value;
}
}