为什么我们在并发环境中读取一个值时需要一个(读)锁?

Why do we need a (read) lock when reading a value in a concurrent environment?

假设以下 class:

public class Counter {
    private long val;
    private final ReadWriteLock reentrantLock = new ReentrantReadWriteLock();

    public Counter(long val) {
        this.val = val;
    }

    public void increment() {
        try {
            reentrantLock.writeLock().lock();
            val++;
        } finally {
            reentrantLock.writeLock().unlock();
        }
    }

    public long getVal() {
        try {
            reentrantLock.readLock().lock();
            return this.val;
        } finally {
            reentrantLock.readLock().unlock();
        }
    }
}

忽略我们可以使用 AtomicLong,当我们在没有锁定的情况下读取时会发生什么不好的事情,为什么会发生这些事情。

我的假设:

  1. 不是最新的值(一个新的写入线程可以在我们读取的那一刻更新值+1),所以我们至少落后一个值
  2. 写入值之间存在一些垃圾(这会发生在 java 中)

另一个假设:

有什么我想念的吗?

(理论上)可能发生的坏事是,如果 readers 不使用读锁,他们可能会看到计数器的陈旧值;即不是作者写入的最新值的值。

在Java中,原始锁和Lock类有两个作用:

  1. 提供互斥。

  2. 它们为线程提供有关共享变量中值可见性的某些保证。

如果没有 正确 使用锁(和其他一些东西)提供的可见性保证,一个线程所做的更改可能对另一个线程不可见。


不幸的是,虽然不能保证一个线程不会看到正确的值,但也不能保证它会看到不正确的值。实际行为取决于许多难以分析的不同因素……并且取决于实现和平台。因此,证明 线程可以看到陈旧值可能很困难。同样,您不能通过测试来证明 一个程序没有那种缺陷。如果一个程序 确实 有这种缺陷,它可能很难重现......尤其是当你使用调试器时。


Not the latest value (a new writer thread could update the value +1 in the moment we read), so we would be at least one value behind.

事实上,reader 可以看到很多更新落后的值...甚至 val 的初始值。

Some garbage in-between writes value (can that happen in java)

这也是可以的。 JMM 将 longdouble 视为两个独立的内存单元,因此不使用锁的 reader 可以从一个值中看到高字,从另一个值中看到低字值。

There's no risk of not seeing any writer value, since the WriteLock enforces memory barriers which will flush the value to main memory.

从两个方面来看这是不正确的:

  1. 这是一个实现细节。 JMM 没有提到内存屏障,相关的 javadocs 也没有。

  2. 事实上,如果 reader 没有使用读锁,JIT 编译器 可能 发出缓存 [=11 的值的代码=] 在寄存器中...并且在任何情况下都不必费心从主内存中重新读取它。

    请注意,这也是一个实现细节。但这是 JMM 允许 的行为。当reader不使用锁时,写和后续读之间不会有happens before关系。如果没有这种关系,代码就不需要满足可见性保证。