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

15

示例代码:

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;
    }
}

是的。但不仅仅是ReentrantLock,还包括来自Eclipse RCP框架的ISchedulingRule和ILock。 - Temple Wing
5个回答

10

根据文档

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

  • 成功的lock操作类似于成功的monitorEnter操作
  • 成功的unlock操作类似于成功的monitorExit操作
如果您在两个线程中都使用Lock(即读和写线程),则读线程将看到新值,因为monitorEnter会刷新缓存。否则,您需要声明变量volatile以强制在读取线程中从内存中读取。

我可以再问一点吗?
您能详细解释一下“清除缓存”的含义吗?
这是否意味着无论哪个线程进入监视器,所有处理器都会刷新其缓存?
还是只有与进入的监视器相关的缓存数据将被清除?
- Temple Wing
@TempleWing 我的理解是JVM在尝试进入监视器之前会刷新缓存,因此,是的,所有竞争的线程都会将其缓存刷新到内存中,然后逐个进入锁。当然,如果CPU不运行尝试进入锁的线程,则该CPU的缓存不会被刷新。 - Sergey Kalinichenko
我认为JVM不会清空缓存,而是使用内存屏障提供可见性。 - cozos
1
@cozos 都不是正确的描述。JVM只需采取必要的措施,以确保内存可见性。因此,如果优化执行表现为有一个线程本地缓存,那么锁定/解锁操作就必须表现为刷新了该缓存。值得注意的是,优化器可能会完全消除内存访问,而不是操作缓存。例如,当条件可预测时,条件分支可能会被消除,这表现为条件值被缓存,而实际上评估根本没有发生。(解)锁定限制了JVM的优化。 - Holger

1

根据布莱恩法则...

如果你正在写一个变量,可能会被另一个线程读取,或者正在读取一个变量,可能是由另一个线程最后写入的,那么你必须使用同步,并且进一步地,读者和写者都必须使用相同的监视器锁进行同步。

因此,同步设置器和获取器都是合适的......

或者

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


1
如果我有一个线程不断调用getV,并且在另一个线程中只执行一次setV,那么读取线程是否保证在写入后立即看到新值?
不是的,读取线程可能只读取其自己的副本(由运行读取线程的CPU核心自动缓存),因此无法获取最新的值。
或者我需要将“V”设置为volatile或AtomicReference吗?
是的,它们都可以起作用。
将V设置为volatile仅会阻止CPU核心缓存V的值,即对变量V的每次读写操作都必须访问主内存,这会降低速度(大约比从L1缓存读取慢100倍,请参见 interaction_latency了解详情)
使用V = new AtomicInteger()有效是因为AtomicInteger在内部使用private volatile int value;来提供可见性。

如果您在读写线程上使用锁(Lock对象、synchronized块或方法;它们都可以工作),那么第二个代码段所示的方式也可以工作,因为(根据Java®虚拟机规范第二版8.9节)

...锁定任何锁都会从线程的工作内存中概念性地刷新所有变量,并且解锁任何锁都会将线程分配的所有变量的写入强制刷出到主内存...

...如果线程在锁定特定锁之后并在相应的解锁之前仅使用特定共享变量,则线程将在锁定操作之后必要时从主内存中读取该变量的共享值,并将最近分配给该变量的值复制回主内存,在解锁操作之前。这与锁的互斥规则结合使用足以保证通过共享变量正确地从一个线程传递值到另一个线程...

P.S. AtomicXXX类还提供CAS(比较并交换)操作,这对于多线程访问非常有用。

P.P.S. 该主题的JVM规范自Java 6以来未发生变化,因此它们不包括在Java 7、8和9的JVM规范中。

P.P.P.S. 根据这篇文章,CPU缓存始终是一致的,无论从每个核的视角来看。你问题中的情况是由“内存排序缓冲区”引起的,在其中storeload指令(分别用于从内存中写入和读取数据)可能会被重新排序以提高性能。具体而言,该缓冲区允许load指令超越早期的store指令,这正是导致问题的原因(getV()被放在前面,所以它在另一个线程中更改值之前读取了该值)。然而,我认为这更难理解,因此“不同核的缓存”(如JVM规范所示)可能是更好的概念模型。


1
请查看以下问题以获取更多讨论:https://dev59.com/kHI-5IYBdhLWcg3wcn7m - Song JingHe

0
你应该使用volatile或者AtomicInteger。这将确保读取线程最终能够看到更改,并足够接近“之后”以适用于大多数情况。从技术上讲,对于像这样的简单原子更新,您不需要锁。仔细查看AtomicInteger的API。set()、compareAndSet()等都将原子地设置值,使其可被读取线程看到。

0

显式锁,synchronized, 原子引用和 volatile,都提供内存可见性。锁和 synchronized 对它们所包围的代码块提供内存可见性,原子引用和 volatile 则针对特定声明的变量提供内存可见性。然而为了使可见性正常工作,读取和写入方法都应该由相同的锁对象保护。

在你的情况下它不起作用,因为你的获取方法没有被保护的锁所保护。如果你进行更改,它将按照所需工作。同时,只声明变量为 volatileAtomicIntegerAtomicReference<Integer> 也可以起到作用。


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