获取器和设置器是否应该同步?

75
private double value;

public synchronized void setValue(double value) {
    this.value = value;
}
public double getValue() {
    return this.value;
}
在上面的例子中,使getter同步有意义吗?

1
如果您使用的是Java 1.5或更高版本,并且仅使用set和get方法(则setValue不必同步),那么将双字段设置为volatile将是令人满意的。请参阅java.util.concurrent.atomic.Atomic*类以及已经引用的《Java并发实践》。 - s106mo
@s106mo 这在所有的Java版本上都可以运行。 - Marko Topolnik
如果您只是在寻找用于受保护数据的设置和获取方法的同步方式,可以查看以下替代方案:https://dev59.com/9mkw5IYBdhLWcg3woMFD#37395582 - Ravindra babu
4个回答

93
我认为最好引用Java Concurrency in Practice这里的内容:
“假设只有在写入共享变量时才需要使用同步是一个常见的错误,这只是不正确的。对于可能被多个线程访问的每个可变状态变量,所有对该变量的访问都必须使用相同的锁来执行。在这种情况下,我们说该变量由该锁保护。”
“在没有同步的情况下,编译器、处理器和运行时会对操作出现的顺序进行一些非常奇怪的事情。尝试推断内存操作“必须”在不充分同步的多线程程序中发生的顺序几乎肯定是不正确的。”
“通常,您不必对原始数据类型如int或boolean进行如此小心。如果这是int或boolean,则可能是这样:当线程在没有同步的情况下读取变量时,它可能会看到过时的值,但至少它会看到一个实际上由某个线程放置在那里的值,而不是一些随机值。”
然而,在64位操作中,例如对于longdouble(如果它们没有声明为volatile),情况并非如此。Java内存模型要求获取和存储操作是原子的,但对于非易失性的长整型和双精度浮点型变量,JVM允许将64位读取或写入视为两个独立的32位操作。如果读取和写入发生在不同的线程中,则有可能读取非易失性长整型并返回一个值的高32位和另一个值的低32位。因此,即使您不关心陈旧的值,除非这些变量被声明为易失性或受锁保护,否则在多线程程序中使用共享可变的长整型和双精度浮点型变量是不安全的。

11
更详细地说,在多线程应用中,如果想获取最新版本的值,getter和setter都应该进行同步。如果出于某些非常奇怪的原因你不关心获取最新版本,而且同步会占用太多时间(这种情况不太可能!),我认为你可以不同步getter。但是在这种情况下,双精度浮点数有64位,因此它应该始终进行同步。 - user949300
7
不同步getter方法是完全错误的做法。你永远不可能观察到变量的任何更改。 - Marko Topolnik
2
@user949300 不仅无法获取最新版本,而且您甚至不知道将获得哪个版本 - 而该版本可能会因线程而异。这几乎不是一个可以接受的情况... - assylias
2
在缺乏同步的情况下,编译器、处理器和运行时可能对操作出现的执行顺序做出一些非常奇怪的事情,可以参考文档http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4.5中的示例17.4.5-1(Happens-before Consistency)。 - Martin Wilson
2
无论JVM是32位还是64位,在我们讨论规范级别时都没有关系。JLS将longdouble操作定义为非原子操作。但是,所有这些在新的内存模型定义中都不那么重要,因为清楚地指出没有适当的“happens-before”顺序就没有交易。 - Marko Topolnik
显示剩余16条评论

21

让我通过示例向您展示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,它可以忽略其他线程的任何操作。


3
有没有官方的资料显示在没有synchronized的情况下这种排列是可能的,而synchronized则会阻止它们发生? - dhblah
3
每天你都在执行代码,类似这样的事情(甚至更加可怕的事情)经常发生。 - Marko Topolnik
@b1naryatr0phy 你所需要的只是JLS,特别是其中的内存模型部分。所有允许的优化都是该模型的结果。 - Marko Topolnik
这种优化怎么可能是合法的?即使在单线程场景下也是非法的。编译版本中while循环内的“某些操作”可能会改变myBean的值。然而,在通过if条件后,即使值已经发生了改变,也没有被检查,即使它可能在while循环内发生了改变。即使值在while循环内没有改变,但可能会被不同的线程改变 - 如何在getValue()上同步避免进入无限循环? - Ajoy Bhatia
@AjoyBhatia,“一些操作”可能会或可能不会更改myBean,JIT 编译器会知道这个情况。如果它确定“一些操作”可能会更改 myBean,那么就不会进行优化。至于其他线程,这是基本的 Java 内存模型语义:如果没有同步/易失性,运行时在推理代码时可以忽略其他线程的操作。 - Marko Topolnik
显示剩余9条评论

2
这里的原因是为了防止在一个线程读取时,其他任何线程更新值,并避免对过期值执行任何操作。
在此处,获取方法将获得“this”的内部锁,因此任何其他尝试使用setter方法设置/更新的线程都必须等待获取“this”的锁才能进入已由执行get操作的线程获取的setter方法。
这就是为什么建议在对可变状态执行任何操作时遵循使用相同锁的做法。
在这里使字段易变将起作用,因为没有复合语句。
需要注意的是,同步方法使用内部锁即”this”。因此,get和set都被同步意味着进入方法的任何线程都必须获取此锁。
执行非原子64位操作时应特别注意。以下摘自Java并发实践可能有助于理解情况-
“Java内存模型要求抓取和存储操作是原子性的,但对于非易失性长整型和双精度浮点数变量,JVM可以将64位读取或写入视为两个单独的32位操作。如果读取和写入发生在不同的线程中,则有可能读取非易失性长整型并返回一个值的高32位和另一个值的低32位。因此,即使您不关心过期值,也不安全在多线程程序中使用共享的可变长浮点数和双精度浮点数变量,除非它们声明为易失性或由锁保护。”

-3
也许对于某些人来说,这段代码看起来很糟糕,但它的工作效果非常好。
  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;
    }
  }

2
确实看起来很糟糕。我不明白这比使用简单的同步一行方法的标准做法更好在哪里:这是两倍的代码,可读性远低于常见的Java实践(因此会分散熟悉常见Java实践的读者的注意力)。它还在Double.class上进行同步,这实际上使其成为全局锁而不是每个实例锁。真正的意义何在? - jkff
请检查主题。类同步不会阻塞类,只会阻塞同步块。有时我们的getter和setter更加复杂。在方法体中,我们做不同的事情,并且希望在许多线程中保持安全。更新函数将暂停另一个线程,例如如果我们进行读取。请自行检查并确保您不仅通过同步变量来完成此操作。 - Adam111p
1
同步块会阻塞你传递给它的值,而你正在传递 Double.class,这是 JVM 中的全局对象。 - jkff
在我的情况下,它是一个静态方法。类是不同的,例如这里是double。也许不是最好的例子,另一个类将是粘胶。不能责怪的是阻塞只用于同步块。 - Adam111p
好的。我的意思是,这段代码将阻塞同一JVM中任何其他使用synchronized(Double.class)的代码。 - jkff
1
@jkff - 你第一次说的是正确的。这通常被称为全局锁,与某些编程语言中的全局解释器锁不同。但它比实例锁更容易成为瓶颈。因此,从性能角度来看,这种解决方案绝对劣于实例锁...而且很“奇怪”。 - Stephen C

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接