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



示例代码:

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"变为volatile还是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;
    }
}

来自文档:

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

  • 成功的锁定操作就像成功的监视器输入操作
  • 成功的解锁操作就像成功的监视器退出操作

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

根据布莱恩定律。。。

如果您正在编写一个变量,下一个变量可能会被另一个变量读取线程,或者读取可能最后一次写入的变量另一个线程,您必须使用同步,此外读取器和写入程序必须使用相同的监视器锁进行同步。

因此,同步setter和getter是合适的

如果要避免lock-unlock块(即同步块)

,请使用AtomicInteger.incrementAndGet()

如果我有一个线程不断调用getV,而我只在另一个线程,该读取线程是否保证看到新值刚写完?

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

或者我需要使"V"变为volatile还是AtomicReference?

是的,它们都有效。

使V易失性只需停止CPU Core缓存V的值,即对变量V的每次读/写操作都必须访问主内存,这比从一级缓存读取慢大约100倍,有关详细信息,请参阅interaction_latency

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

而且,如果您在读写线程上使用锁(Lock对象、synchronized块或方法;它们都有效)(就像您的第二个代码段一样),它也有效,因为(根据Java®虚拟机规范第二版第8.9节)

从概念上讲,锁定任何锁都会从线程的工作记忆,解锁任何锁都会迫使写入main线程分配的所有变量的内存。。。

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

另外,AtomicXXX类还提供CAS(比较和交换)操作,这对于多读访问非常有用。

p.p.S.关于这个主题的jvm规范自Java6以来没有改变,所以它们没有包含在Java7、8和9的jvm规范中。

p.p.p.S.根据这篇文章,无论从每个核心的角度来看,CPU缓存总是一致的。您问题中的情况是由"内存排序缓冲区"引起的,其中store&load指令(相应地,用于从存储器写入和读取数据)可以重新排序以提高性能。详细地说,缓冲区允许load指令领先于旧的store指令,这正是导致问题的原因(getV()被放在前面,所以它在您在另一个线程中更改值之前读取该值)。然而,在我看来,这更难理解,所以"不同内核的缓存"(正如JVM规范所做的那样)可能是一个更好的概念模型。

您应该将其设为volatile或AtomicInteger。这将确保阅读线程最终会看到变化,并在大多数情况下足够接近"紧跟其后"。从技术上讲,像这样简单的原子更新不需要锁。仔细看看AtomicInteger的API。set()、compareAndSet()等都将通过原子读取线程来设置可见值。

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

它在您的情况下不起作用,因为保护setter方法的锁没有投影getter方法。如果您进行更改,它将按要求工作。同样,仅将变量声明为volatileAtomicIntegerAtomicReference<Integer>也可以。

最新更新