考虑以下代码:
public class MyDataStructure {
int size;
final ReentrantLock lock = new ReentrantLock();
public void update() {
lock.lock();
try {
// do coll stuff
size++;
} finally {
lock.unlock();
}
}
public int size() {
return size;
}
}
据我了解,ReentrantLock
强加了发生之前的关系,因此 size++
需要用新值影响主内存和(其他线程的)缓存。因此,无需使用 volatile
作为大小
。此外,出于同样的原因,方法 size()
可以简单地返回 size
。
我的理解正确吗?
最佳答案
没有。
其他线程可能会看到过时(旧)的大小值。
当其他线程执行 size()
时,没有任何指示它确保它不读取旧值,例如另一个 cpu 缓存中的本地缓存值等。还值得一提的是提升jvm。
举个例子,如果你有一个在一个线程中调用 size()
的循环,如果循环中没有调用 update()
(或者 size直接更改),并且仅从其他线程调用/更改。
while (size() == 0) {
...
}
jit 编译器(至少是过去被称为 c2/server 编译器的现代部分)可以愉快地优化(提升)大小变量检查,因为它发现它在循环中永远不会改变。
有关替代方案的更新:
volatile
可能会有所帮助如果只有一个线程会写入变量大小,包括调用update()
。否则它不会受到保护,因为 size++
都是先读取然后更新(写入)变量,因此两个线程仍然可以“同时”读取具有相同值的新副本,并且都添加+1,但不是 +2,而是总共 +1。
即使只有一位作者,我也会反对它,因为这在未来可能会改变,并且在代码中是一件相当微妙的事情,因此 future 的开发人员(包括自己)将面临错过这一点的巨大风险。
因此,一种选择可能是向 size()
函数添加锁定。可能很好地利用了锁的额外功能,或者甚至使用允许许多读者但只有一个作家等的读写锁。它不像其他替代方案那样可读(而且相当冗长)。如果这是一个代价高昂的操作(但在本例中并非如此),那么 future 的虚拟线程也可能会很好。
另一个选择只是传统的简单方法,将 synchronized
添加到两个方法,或者 synchronized (this) { ... }
block ,这无疑会提供保证排除和内存可见性。唯一真正的缺点:这意味着使用该对象实例作为互斥体/监视器,因此如果其他不相关的变量也可能需要保护或其他引用该对象的变量可能需要保护,则可能不够精细。然后,您可以为每个对象添加特殊的监视器/互斥对象。模式,每个字段:
private final Object sizeMutex = new Object();
...并在需要时同步那些synchronized (sizeMutex) { ... }
,但随后它开始变得越来越冗长/复杂,但仍然相当明显和易于理解。最大的风险是可能引发僵局。很可能甚至不需要那么细致,但值得考虑。
在这种特殊情况下,最简单的选择是使用 AtomicInteger 或其他基元甚至整个对象的相关类,正如其他人在评论中建议的那样。
private final AtomicInteger size = new AtomicInteger();
size() {
return size.get();
}
update() {
size.incrementAndGet();
}
关于java - 我需要使用 volatile 吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/73777728/