英文:
Why does this code get stuck in infinite loop
问题
public void bad() {
final ConcurrentMap<String, Integer> chm = new ConcurrentHashMap<>();
final String key = "1";
chm.computeIfAbsent(key, __ -> {
chm.remove(key);
return 1;
});
}
当运行此代码时,您会在调用http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/java/util/concurrent/ConcurrentHashMap.java#l1096的第1107行陷入无限循环。
public void bad2() {
final ConcurrentMap<String, Integer> chm = new ConcurrentHashMap<>();
final String key = "1";
Thread worker = new Thread(() -> chm.remove("1"));
chm.computeIfAbsent(key, __ -> {
worker.start();
try {
worker.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
return 1;
});
}
为什么在这两种情况下,chm.remove(key)都没有正常完成并返回null,然后设置值为1?
有趣的是,在某个时候解决了这个问题,当我使用Java 17运行第一个示例时,会抛出java.lang.IllegalStateException: Recursive update。
英文:
public void bad() {
final ConcurrentMap<String, Integer> chm = new ConcurrentHashMap<>();
final String key = "1";
chm.computeIfAbsent(key, __ -> {
chm.remove(key);
return 1;
});
}
Of course i understand this looks very silly. It is just a super simplified version of some problematic code i was dealing with. I understand it makes no sense to do this but i am trying to understand the behaviour it causes.
When running this code you get stuck in an infinite loop on line 1107 after invoking http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/java/util/concurrent/ConcurrentHashMap.java#l1096
I am finding it very difficult to understand exactly what is happening which is causing this. Same behaviour when done on a seperate thread but waiting
public void bad2() {
final ConcurrentMap<String, Integer> chm = new ConcurrentHashMap<>();
final String key = "1";
Thread worker = new Thread(() -> chm.remove("1"));
chm.computeIfAbsent(key, __ -> {
worker.start();
try {
worker.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
return 1;
});
}
Why in both cases is it not that chm.remove(key) completes normally returning null and then the value of 1 is set?
Interestingly this was addressed at some point and the first example throws java.lang.IllegalStateException: Recursive update when i ran with java 17.
答案1
得分: 3
这被称为“合同错误”。这种情况发生在Javadoc明确告诉你不要做某事X,但只是这样告诉你,它没有指定如果你做X会发生什么,只是不应该这样做。在这种情况下,X是“在传递的函数中更新映射”,而Javadoc明确说明:不要这样做。
因此,计算应该简短而简单,并且不得尝试更新此映射的任何其他映射。
当发生这种合同错误时,任何事情都可能发生。规范很清楚:不要这样做。因此,如果你这样做,规范实际上宣称有能力做“任何事情”。严重崩溃,从扬声器中吹口哨曲,等等。
这就是行为发生变化的原因(通常情况下,Java不会改变其行为而不进行相当大的兼容性破坏,但在这里,规范的行为没有改变,因为规范仅仅是说“不要这样做,如果您不听取这个警告,此事将不按规范执行”,而“无限循环”和“抛出异常”在这方面都只是“执行未指定的损坏操作”)。
好的,但是为什么会无限循环?
因为并发哈希映射是“智能的”并使用重试/CAS更新模型。它不会获取一堆东西,而只是尝试操作而不执行操作,但随后/之后将检查是否实际成功,或者由于其他线程在同一时间在相同的一般区域修改了相同的映射,其写入被覆盖或未应用,如果是这样,它将再次尝试。在这种情况下,移除键实际上是“消除标记”,这使得CHM认为它并发更新了另一件事,因此它应该再次尝试。永远永远。
这就是你链接的源文件中第1656行的“cas”所代表的:比较和设置。这是一种比锁定快得多的并发原语:“如果当前值为X,那么将其设置为Y。否则,根本不要设置它。告诉我你是否设置了它或没有” - 所有这些都在一个原子操作中完成,因为CPU通常以基本机器代码支持它,所以速度很快。不需要锁定获取。一般的原则是检查当前值是什么,进行一些簿记,然后设置新值,使用CAS来确保我们检查的“状态”仍然是我们所处的状态。如果不是,某个其他线程碰巧也在更新内容,所以重新开始。
这只是一个实现。明天,它可能会改变。你不能依赖“它会无限循环”,因为规范不保证它,事实上,在JDK17中,你会得到一个异常。
英文:
This is called a 'contract error'. This happens when javadoc explicitly tells you NOT to do thing X, and leaves it at that; it does not specify what happens if you do X, just that you shouldn't do that. In this case, X is 'update the map in your passed-in function', and the javadoc explicitly spells out: DO NOT.
> so the computation should be short and simple,
> and must not attempt to update any other mappings of this map.
When you perform such a contract error, anything can happen. The spec is clear: Don't. So, if you do, the spec essentially claims the ability to do anything. Hard-crash, whistle a dixie tune from the speakers, you name it.
Hence why the behaviour changed (ordinarily, java does not change its behaviours without quite a big ordeal about breaking compatibility, but here, the spec behaviour has not changed, because the spec merely says 'do not do this, this thing does not perform according to spec if you fail to heed this warning' and 'loop endlessly' and 'throw an exception' are both just 'doing unspecified broken stuff' in this regard.
Okay, but why does it endlessly loop?
Because concurrent hashmap is 'smart' and uses a retry/CAS update model. Instead of acquiring a bunch of stuff, it just tries the operation without doing that, but will then check during/afterwards if it actually succeeded, or if, due to other threads modifying the same map at the same time in the same general area, its write got overwritten or otherwise didn't apply, in which case it'll do it again. In this case, removing the key is essentially 'eliminating a marker', which makes CHM think it updated a thing concurrently with another thing and therefore it should try again. Forever and ever.
That's what the 'cas' in line 1656 in your linked source file (casTabAt
) stands for: Compare-And-Set. This is a concurrency primitive that can be a lot faster than locks: "If the current value is X, then set it to Y. Otherwise, do not set it at all. Tell me whether you set it or not" - all that, in one atomic operation, which is speedy because CPUs tend to support it as barebones machine code. No lock acquiry required. The general principle is to check what the current value is, do some bookkeeping, then set the new value, using CAS to ensure that the 'state' you checked is still the state we're in. If not, some other thread so happened to also be updating stuff, so, start over.
That's just one implementation. Tomorrow, it can change. You cannot rely on 'it will endlessly loop' because the spec do not guarantee it, and indeed, in JDK17, you get an exception instead.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论