Java通过其原子类公开CAS操作,例如
boolean compareAndSet(expected,update)
JavaDocs指定compareAndSet操作的内存效果,如下所示:
compareAndSet和所有其他读取和更新操作 例如getAndIncrement具有两者的记忆效应 读写易变变量。
这肯定适用于成功的compareAndSet
调用。但是如果compareAndSet返回false
?
我会说不成功的compareAndSet
对应于易失性读取(因为在这种情况下必须访问原子实例的当前值),但我不明白为什么CAS应该执行特殊的内存屏障不成功案件中的说明。
问题实际上是,不成功的CAS是否也建立了先发生过的关系。请考虑以下程序:
public class Atomics {
private static AtomicInteger ai = new AtomicInteger(5);
private static int x = 0;
public static void main(String[] args) {
new Thread(() -> {
while (x == 0) {
ai.compareAndSet(0, 0); // returns false
}
}, "T1").start();
new Thread(() -> {
x = 1;
ai.compareAndSet(0, 0); // returns false
}, "T2").start();
}
}
线程T2(和程序)肯定会终止吗?
答案 0 :(得分:1)
使用volatile
读取和写入建立发生在之前的关系的问题是,这种关系仅存在于写入和后续读取中。如果一个线程T1写入共享volatile
变量而另一个线程T2从同一个变量读取,则如果T2在T1写入之前读取该变量,则不会有发生在之前的关系它。如果确定T1在T2读取之前是否写入的是线程调度,那么我们就没有任何保证。
在没有额外同步的情况下处理它的实际方法是评估实际值,T2已读取。如果此值明显表明T1已经写入了新值,那么我们就会有一个有效的发生之前的关系。这是使用volatile boolean fooIsInitialized
标志或volatile int currentPhase
计数器时的工作原理。很明显,如果写入的值与旧值相同或者从未实际写入新值,则此操作无效。
您的示例程序的问题在于它推测了线程调度。它假设T2最终执行cas动作,并且在T1中将存在后续迭代,其中下一个cas将创建发生在之前的关系。但这不能保证。它可能不是直观易懂的,但是如果没有同步,T1的所有迭代都可能在T2的动作之前发生,即使循环是无限的。它甚至是一个有效的线程调度行为,让T1在将CPU时间分配给T2之前永远消耗100%的CPU时间,因为不能保证相同优先级的线程之间的抢先线程切换。
但是即使底层系统确实将CPU时间分配给最终将执行操作的T2,也不需要JVM将其显示给T1,因为T2无法观察到T1曾经运行过的T1。它不太可能在现实生活中发现这一点,但答案仍然是没有保证。当有一系列动作可以让T2观察到T2运行(即改变其状态)时,情况会发生变化,但当然,这一系列动作会使cas过时。
答案 1 :(得分:0)
我喜欢霍尔格的回答,并且由于技术信息会接受,但我会用相同的结果写一个答案,但不同的观点。这个程序有可能永远运行吗?是的,请考虑可能的编译器重新排序。
new Thread(() -> {
if(x == 0){
while (true) {
ai.compareAndSet(0, 0); // returns false
}
}
}, "T1").start();
这可能是重新订购吗?是的。尽管有一个后续的易变商店,但没有规则说这里的升降机不会发生。这里唯一的规则是无法在易失性存储下面执行读取。
编辑:我意识到问题是关于记忆效应,而不是编译器排序。我会留下这个答案,因为它可能有用,但并没有真正回答这个问题。