Java 如果没有争用,则在读取时是否需要同步

Java 如果没有争用,则在读取时是否需要同步,java,multithreading,concurrency,locking,concurrent-programming,Java,Multithreading,Concurrency,Locking,Concurrent Programming,考虑下面的代码狙击手: package sync; public class LockQuestion { private String mutable; public synchronized void setMutable(String mutable) { this.mutable = mutable; } public String getMutable() { return mutable; } }

考虑下面的代码狙击手:

package sync;

public class LockQuestion {
    private String mutable;

    public synchronized void setMutable(String mutable) {
        this.mutable = mutable;
    }

    public String getMutable() {
        return mutable;
    }   
}
时间1线程Thread1将更新'mutable'变量。在setter中需要同步,以便将内存从本地缓存刷新到主内存。 在时间Time2(Time2>Time1,无线程争用)时,线程Thread2将读取可变的值


问题是——我需要在getter之前放置synchronized吗?看起来这不会引起任何问题-内存应该是最新的,Thread2的本地缓存应该由Thread1失效和更新,但我不确定。

与其奇怪,为什么不在中使用原子引用呢


(值得一提的是,我对的阅读并不能保证Thread2会看到对mutable的更改,除非它也使用synchronized…但是我总是对JLS的这一部分感到头疼,所以使用原子引用)

你绝对确定只有在调用setter之后才会调用getter吗?如果是这样,就不需要同步getter,因为并发读取不需要同步


如果有可能同时调用get和set,那么您肯定需要同步这两个函数。

如果您将可变变量设置为volatile,则没有问题,“

中的详细信息可能永远不会导致错误行为,但除非您还保证线程启动的顺序,您不能保证编译器在写入线程1之前没有对读入线程2重新排序。更具体地说。因此,只要线程在优化下连续运行相同的输出,整个语言堆栈(编译器、硬件、语言运行时)就可以完成 差不多是它想要的。包括允许Thread2缓存
LockQuestion.getMutable()
的结果

实际上,如果真的发生了这种情况,我会非常惊讶。如果要保证不会发生这种情况,请将
LockQuestion.mutable
声明为
final
,并在构造函数中初始化:


如果您非常担心读取线程中的性能,那么您要做的就是使用适当的同步或volatile或原子引用读取一次值。然后将该值赋给一个普通的旧变量


对普通变量的赋值保证在原子读取之后发生(因为它如何才能获得值?),如果该值再也不会被另一个线程写入,则所有设置都已设置。

我认为您应该从正确的内容开始,并在以后发现问题时进行优化。我只会使用原子参考,除非几纳秒太长


ps是皮秒,是百万分之一微秒。

虽然原子类非常有用,但它们不可避免地会有微小的性能损失。我更喜欢使用“简单”类型,除非需要原子性更新。在上述情况下,只需要更新的可见性。@Petro Semeniuk没有原子对象、同步和/或易失性,无法保证更新和读取的“可见性”。这可能重要,也可能不重要。请注意,
synchronized
/
volatile
需要在同一个对象上。Petro,如果您真正关心内存屏障的性能,我希望我有您的性能问题。这不是真的。如果getter中没有同步锁,那么在保证之前就不会发生任何事情。setter可以激发、完成,然后getter在另一个线程中激发——但是读取旧的值。这可能重要,也可能不重要。对于单次访问,
volatile
在这里是“等效的”,但忽略了更大的同步影响。OP写道,setter在Time1调用,getter在Time2调用,Time2>Time1,因此线程2可能只在线程1完成后启动,这提供了“before”保证(例如,thread1.Start()、thread1.Join()、thread2.Start())我看不到任何与正在停止或连接的线程相关的内容。T2>T1对于之前发生的事件是不够的--缓存会将此假设抛出窗口。OP写道“无线程争用”所以线程必须以某种方式同步才能保证这一点,但我同意没有提供足够的信息说明这是如何完成的,这就是为什么我在我的帖子中给出了两个答案。我没有提供足够的信息,因为我没有这些信息。@pst你对之前发生的事情是正确的。T2>T1不能保证这一点,而访问/发布相同的lock(通过volatile/synchronized)或使用原子类可以保证这一点。谢谢。我对volatile的想法与原子类类似——它们很便宜,但不是免费的。理想情况下,我想强制“在之前发生”,但不会对读取进行任何惩罚/锁定。@Petro Semeniuk这不是关于“免费”,而是关于“始终具有所需的语义”(例如,不让奇怪的同步错误进入)。然而,您的程序可能在没有“昂贵的锁”的情况下运行良好(面对它,除非高度竞争,否则它非常便宜)——但这只是供您从更大的角度进行分析。最好只做“正确的事情”"不必担心愚蠢的微优化。虽然在实践中<代码>易失性< /代码>很少有用。@汤姆:为什么?我成功地使用了<代码> Value成功地进行同步。因为我确切地知道它的语义,这应该是什么问题?它是一个很好的轻量级同步机制。顺便问一下,你会考虑吗?
ConcurrentHashMap
“很少有用”?@Roland Illig
ConcurrentHashMap
的用处与
volatile
的用处有什么关系?Java中的线程是根据先发生后关系定义的。不要试图从刷新缓存的角度来考虑,因为你会错的。首先,编译器优化很重要。即使刷新缓存s是一个精确的数字
private static class LazySomethingHolder {
  public static Something something = new Something();
}

public static Something getInstance() {
  return LazySomethingHolder.something;
}
public static void main(String... args) {
    AtomicReference<String> ars = new AtomicReference<String>();
    ars.set("hello");
    long start = System.nanoTime();
    int runs = 1000* 1000 * 1000;
    int length = test(ars, runs);
    long time = System.nanoTime() - start;
    System.out.printf("get() costs " + 1000*time / runs + " ps.");
}

private static int test(AtomicReference<String> ars, int runs) {
    int len = 0;
    for (int i = 0; i < runs; i++)
        len = ars.get().length();
    return len;
}
get() costs 1219 ps.