显式锁定是否自动提供内存可见性?

时间:2012-09-14 18:13:37

标签: java multithreading concurrency

示例代码:

class Sample{
    private int v;
    public void setV(){
        Lock a=new Lock();
        a.lock();
        try{
            v=1;
        }finally{
            a.unlock();
        }
    }
    public int getV(){
        return v;
    }
}

如果我有一个线程不断调用getV而我只是在另一个线程中执行setV一次,那么读取线程是否保证在写入后立即看到新值?或者我需要使“V”易变或AtomicReference?

如果答案是否定的,那么我应该将其改为:

class Sample{
    private int v;
    private Lock a=new Lock();
    public void setV(){
        a.lock();
        try{
            v=1;
        }finally{
            a.unlock();
        }
    }
    public int getV(){
        a.lock();
        try{
            int r=v;
        }finally{
            a.unlock();
        }
        return r;
    }
}

5 个答案:

答案 0 :(得分:7)

来自documentation

  

所有Lock实现必须强制执行内置监视器锁提供的相同内存同步语义:

     
      
  • 成功的锁定操作就像成功的monitorEnter操作
  •   
  • 成功的解锁操作就像一个成功的monitorExit操作
  •   

如果在两个线程中使用Lock (即读取和写入),则读取线程将看到新值,因为monitorEnter会刷新缓存。否则,您需要声明变量volatile以强制从读取线程中的内存中读取。

答案 1 :(得分:1)

根据布莱恩定律...

  

如果您正在编写一个可能接下来被另一个读取的变量   线程,或读取可能最后写入的变量   另一个线程,你必须使用同步,进一步,两者   读者和作者必须使用相同的监视器锁同步。

所以同步setter和getter是合适的......

如果您想避免 AtomicInteger.incrementAndGet()块(即同步块),请使用lock-unlock代替

答案 2 :(得分:1)

  

如果我有一个线程不断调用getV,我只需要进行setV   另一个线程,是读取线程保证看到新的值   写完之后呢?

不,读取线程可能只读取自己的副本(由运行读取线程的CPU核心自动缓存)V的值,因此不会获得最新值。

  

或者我是否需要将“V”设为volatile或AtomicReference?

是的,它们都有效。

使V volatile简单地停止CPU Core缓存V的值,即对变量V的每次读/写操作都必须访问主内存,这比较慢(约100x)比从L1缓存读取的速度慢一点,详见interaction_latency

使用V = new AtomicInteger()有效,因为AtomicInteger在内部使用private volatile int value;来提供可见性。

而且,如果你在读取和编写线程时使用lock(Lock对象,synchronized块或方法;它们都可以工作),它也有效(因为你的第二个代码段有),因为(根据到Second Edition of The Java ® Virtual Machine Specification部分8.9)

  

...从概念上锁定任何锁都会刷新线程中的所有变量   工作记忆,并解锁任何锁定强制写入主要   对线程分配的所有变量的记忆......

     

...如果线程仅在锁定a后才使用特定的共享变量   特别锁定,并在相应的解锁之前   锁定,然后线程将从中读取该变量的共享值   锁定操作后的主存储器,如有必要,将复制回来   到主存储器最近分配给该变量的值   在解锁操作之前。这与共同相结合   锁的排除规则,足以保证值   通过共享正确地从一个线程传输到另一个线程   变量...

P.S。 AtomicXXX类还提供CAS(比较和交换)操作,这对mutlthread访问很有用。

P.P.S。自Java 6以来,关于此主题的jvm规范没有更改,因此它们不包含在jvm specification for java 7, 8, and 9中。

P.P.P.S。根据{{​​3}},无论每个核心的视图如何,CPU缓存始终是连贯的。您的问题中的情况是由“内存订购缓冲区”引起的,其中store&可以重新排序load指令(用于从内存中写入和读取数据)以获得性能。详细地说,缓冲区允许load指令超越较早的store指令,这正是导致问题的原因(getV()被提前,因此它在您更改之前读取该值另一个线程)。但是,在我看来,这更难以理解,因此“不同核心的缓存”(如JVM规范所做的那样)可能是一个更好的概念模型。

答案 3 :(得分:0)

你应该使用volatile或AtomicInteger。这将确保阅读线程最终会看到变化,并且在大多数情况下足够接近“紧随其后”。从技术上讲,你不需要像这样的简单原子更新。仔细看看AtomicInteger的API。 set(),compareAndSet()等等......都会通过原子方式读取线程来设置值。

答案 4 :(得分:0)

显式锁,synchronized,原子引用和volatile都提供内存可见性。锁定synchronized对它们所包围的代码块和原子引用以及volatile这样做的特定变量执行此操作。但是,为了使可见性正常工作,读取和写入方法都应该受到相同锁定对象的保护。

它不适用于你的情况,因为你的getter方法不是由保护setter方法的锁投射的。如果您进行更改,它将按要求工作。此外,只需将变量声明为volatileAtomicIntegerAtomicReference<Integer>也可以。