英文:
Is put() method in ConcurrentHashMap also atomic?
问题
在ConcurrentHashMap
中,putIfAbsent()
是原子的。我的问题是,ConcurrentHashMap
中的put()
方法是否也是原子的?
英文:
In ConcurrentHashMap
, putIfAbsent()
is atomic. My question is method put()
in ConcurrentHashMap also atomic?
答案1
得分: 3
以下是您要翻译的内容:
文档中没有明确说明put
或get
是否是原子操作。然而,javadoc中有这样的说明:
> 检索反映了在其开始时保持的最近完成的更新操作的结果。(更正式地说,对于给定键的更新操作与报告更新值的任何(非空)检索之间存在先前发生关系。)
这意味着如果一个线程执行了put
,而另一个线程执行了相同键的get
,那么get
将会看到“放置之前”的状态或者“放置之后”的状态。这有效地意味着get
和put
在彼此之间以及与其他显式原子操作相关时是原子的……所有这些都针对给定的键。实际上,如果情况不是这样的话,那么ConcurrentHashMap
在常规/直观意义上就不是线程安全的。
然而,javadoc并未对涉及不同键的操作提供强有力的保证。因此,原子性受到限制。
这种类型的原子性不是特别有趣或有用的属性。例如,虽然put
和get
各自是原子的,但put
后跟一个get
就不是原子的。很难看出您如何利用这些操作的有限原子性……除了一般的线程安全性之外。
(在我看来,这可能就是他们为什么不费心在javadoc中明确提到get
和put
的原子性的原因。)
更有趣的属性是更复杂操作的原子性(或者没有)。例如,诸如putIfAbsent
、compute
和merge
之类的操作是原子的,但批量操作不是原子的。
英文:
It is not stated explicitly in the documentation that put
or get
are atomic. However, the javadoc states this:
> Retrievals reflect the results of the most recently completed update operations holding upon their onset. (More formally, an update operation for a given key bears a happens-before relation with any (non-null) retrieval for that key reporting the updated value.)
That implies if one thread does a put
and another does a get
with the same key, then the get
will either see the "before the put" state or the "after the put" state. That effectively means that get
and put
are atomic with respect to each other, and with respect to other explicitly atomic operations ... all for a given key. Indeed, if this wasn't the case, then ConcurrentHashMap
would not be thread-safe in the conventional / intuitive sense.
However, the javadocs do not provide strong guarantees for operations involving different keys. Atomicity is therefore limited.
This kind of atomicity not a particularly interesting or useful property. For example, while put
and get
are individually atomic, a put
followed by a get
is not atomic. It is hard to see how you would exploit the limited atomicity of these operations ... beyond general thread-safety.
(IMO, that is probably the reason that they don't bother to explicitly mention the atomicity of get
and put
in the javadoc.)
The more interesting property is the atomicity (or not) of the more complex operations. For example, operations like putIfAbsent
, compute
and merge
are atomic, but the bulk operations are not atomic.
答案2
得分: 0
如果我负责这个 API,我会将它添加到文档中,但是有一个解释:
put
本质上是一个单一的操作。从 java.util.Map
的定义来看,put 的操作是原子的。
当然,仅仅因为某个方法任务以原子方式描述,并不意味着其实现是原子的,对于许多集合框架中的内容,它们并不是原子的。ArrayList 的 add 方法是一个原子的描述,但不是一个原子的实现。
区别在于,add 方法的描述仅仅是“将元素添加到末尾”。而不是“搜索末尾节点,一旦找到,将此元素作为尾引用添加到其中”。也不是“获取列表的大小并记住它。然后,检查此列表的容量,如果容量不足以添加项目,请首先按照增长因子扩展容量,然后将先前获得的‘大小’索引的值设置为传递进来的对象。”
即使后面那个冗长的描述或多或少地正确描述了 ArrayList.add 实际上执行的操作。
因此,ConcurrentHashMap
在类级别和方法级别的文档一起起作用:
put()
被描述为一个原子操作,而 putIfAbsent
的描述则完全不是原子的。它明确地被描述为一个两步的过程;甚至在名称中体现出来!“如果不存在,则添加” - 这是一个两步的名称。因此,文档需要特别指出:尽管它听起来是一个两步操作,但在目的上被视为原子操作。
然后,CHM 的类级别文档表明,所有旨在是原子的操作实际上都是原子实现的。
作为对比,像 putAll
这样的操作被描述为一个多步骤的过程,CHM 的文档并没有明确说明 putAll
是原子的。实际上它并不是。你可以观察到使用 putAll
添加的一半内容已经被添加到了一个单独的列表中(putAll 的行为几乎就像运行 for (var e : in.entries()) put(e.getKey(), e.getValue());
一样)。
英文:
If I was in charge of this API I'd add it to the docs, but there is an explanation:
put
is inherently a singular operation. As in, the definition of what put does (from java.util.Map
's definition) is atomic.
Of course, just because some method task is described in an atomic fashion does not imply that the implementation is atomic, and for a great many things in the collections framework, they aren't. ArrayList's add method is an atomic description, but not an atomic implementation.
The difference is, the description of the add job is simply 'add the element to the end'. It is not 'search for the end node, and once found, add this element as tail reference to it'. It is not 'obtain the size of the list and remember it. Then, check the capacity of this list, if it is insufficient to add items to it, expand the capacity by the growth factor first, then, set the value for the earlier obtained 'size' index to the passed in object.'
Even though that latter long-winded description is more or less properly describing what ArrayList.add actually does.
So, the javadoc of ConcurrentHashMap
at the class level and at the method level work together:
put()
is described inherently as an atomic operation, whilst putIfAbsent
's description is not atomic at all. It's explicitly described as a two-step process; it's even in the name! put, if absent - it's a two-step name. Therefore the docs need to go out of their way to say: Even though it sounds like a two-step operation, consider it atomic in purpose.
And then the class javadoc of CHM say that all operations intended to be atomic are in fact atomically implemented.
To give some contrast, something like putAll
is described as a multi-step process, and the CHM docs do not explicitly state that putAll
is atomic. And indeed it isn't. You can observe half of the stuff you're adding with putAll
having been added in a separate list (putAll acts pretty much exactly as if for (var e : in.entries()) put(e.getKey(), e.getValue());
is run.
答案3
得分: 0
Oracle的ConcurrentHashMap::put(K key, V value)
方法的文档未明确说明此方法的原子性。
让我们亲自来看一下(OpenJDK 11<sub>版本为"11.0.2",2019-01-15</sub>):
V put(K key, V value)
调用 V putVal(K key, V value, boolean onlyIfAbsent)
,后者的代码如下:
/** put和putIfAbsent的实现 */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (ConcurrentHashMap.Node<K,V>[] tab = table;;) {
ConcurrentHashMap.Node<K,V> f; int n, i, fh; K fk; V fv;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new ConcurrentHashMap.Node<K,V>(hash, key, value)))
break; // 空槽位时添加而不加锁
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else if (onlyIfAbsent // 检查第一个节点而不获取锁
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (ConcurrentHashMap.Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
ConcurrentHashMap.Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new ConcurrentHashMap.Node<K,V>(hash, key, value);
break;
}
}
}
else if (f instanceof ConcurrentHashMap.TreeBin) {
ConcurrentHashMap.Node<K,V> p;
binCount = 2;
if ((p = ((ConcurrentHashMap.TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
else if (f instanceof ConcurrentHashMap.ReservationNode)
throw new IllegalStateException("递归更新");
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
正如你所看到的,这是部分原子且线程安全的;但是,它不是完全的原子操作,取决于表的状态、要添加的桶/条目的状态以及其他一些特殊细节。
英文:
Oracle's documentation for ConcurrentHashMap::put(K key, V value)
does not explicitly state about the Atomicity of this method.
Let us see this ourselves (OpenJDK 11<sub>version "11.0.2" 2019-01-15</sub>):
V put(K key, V value)
calls V putVal(K key, V value, boolean onlyIfAbsent)
, which, in turn, looks like this:
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (ConcurrentHashMap.Node<K,V>[] tab = table;;) {
ConcurrentHashMap.Node<K,V> f; int n, i, fh; K fk; V fv;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new ConcurrentHashMap.Node<K,V>(hash, key, value)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else if (onlyIfAbsent // check first node without acquiring lock
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (ConcurrentHashMap.Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
ConcurrentHashMap.Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new ConcurrentHashMap.Node<K,V>(hash, key, value);
break;
}
}
}
else if (f instanceof ConcurrentHashMap.TreeBin) {
ConcurrentHashMap.Node<K,V> p;
binCount = 2;
if ((p = ((ConcurrentHashMap.TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
else if (f instanceof ConcurrentHashMap.ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
As you can see, it's a partially atomic and thread-safe; however, it is not FULLY atomic operation, per se, and it depends on the state of the table, state of the bucket/entry you're adding, and some other peculiar details.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论