为什么我们在并发环境中读取一个值时需要一个(读)锁?
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),所以我们至少落后一个值
- 写入值之间存在一些垃圾(这会发生在 java 中)
另一个假设:
- 没有看不到任何写入器值的风险,因为 WriteLock 强制执行内存屏障,这会将值刷新到主内存
有什么我想念的吗?
(理论上)可能发生的坏事是,如果 readers 不使用读锁,他们可能会看到计数器的陈旧值;即不是作者写入的最新值的值。
在Java中,原始锁和Lock
类有两个作用:
提供互斥。
它们为线程提供有关共享变量中值可见性的某些保证。
如果没有 正确 使用锁(和其他一些东西)提供的可见性保证,一个线程所做的更改可能对另一个线程不可见。
不幸的是,虽然不能保证一个线程不会看到正确的值,但也不能保证它会看到不正确的值。实际行为取决于许多难以分析的不同因素……并且取决于实现和平台。因此,证明 线程可以看到陈旧值可能很困难。同样,您不能通过测试来证明 一个程序没有那种缺陷。如果一个程序 确实 有这种缺陷,它可能很难重现......尤其是当你使用调试器时。
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 将 long
或 double
视为两个独立的内存单元,因此不使用锁的 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.
从两个方面来看这是不正确的:
这是一个实现细节。 JMM 没有提到内存屏障,相关的 javadocs 也没有。
事实上,如果 reader 没有使用读锁,JIT 编译器 可能 发出缓存 [=11 的值的代码=] 在寄存器中...并且在任何情况下都不必费心从主内存中重新读取它。
请注意,这也是一个实现细节。但这是 JMM 允许 的行为。当reader不使用锁时,写和后续读之间不会有happens before关系。如果没有这种关系,代码就不需要满足可见性保证。
假设以下 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),所以我们至少落后一个值
- 写入值之间存在一些垃圾(这会发生在 java 中)
另一个假设:
- 没有看不到任何写入器值的风险,因为 WriteLock 强制执行内存屏障,这会将值刷新到主内存
有什么我想念的吗?
(理论上)可能发生的坏事是,如果 readers 不使用读锁,他们可能会看到计数器的陈旧值;即不是作者写入的最新值的值。
在Java中,原始锁和Lock
类有两个作用:
提供互斥。
它们为线程提供有关共享变量中值可见性的某些保证。
如果没有 正确 使用锁(和其他一些东西)提供的可见性保证,一个线程所做的更改可能对另一个线程不可见。
不幸的是,虽然不能保证一个线程不会看到正确的值,但也不能保证它会看到不正确的值。实际行为取决于许多难以分析的不同因素……并且取决于实现和平台。因此,证明 线程可以看到陈旧值可能很困难。同样,您不能通过测试来证明 一个程序没有那种缺陷。如果一个程序 确实 有这种缺陷,它可能很难重现......尤其是当你使用调试器时。
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 将 long
或 double
视为两个独立的内存单元,因此不使用锁的 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.
从两个方面来看这是不正确的:
这是一个实现细节。 JMM 没有提到内存屏障,相关的 javadocs 也没有。
事实上,如果 reader 没有使用读锁,JIT 编译器 可能 发出缓存 [=11 的值的代码=] 在寄存器中...并且在任何情况下都不必费心从主内存中重新读取它。
请注意,这也是一个实现细节。但这是 JMM 允许 的行为。当reader不使用锁时,写和后续读之间不会有happens before关系。如果没有这种关系,代码就不需要满足可见性保证。