In ConcurrentHashMap
of JDK 8, the methods tabAt
and setTabAt
are used to provide volatile read/write of the first element of bins in Node<K,V>[] table
. However, the authors comments that:
Note that calls to
setTabAt
always occur within locked regions, and so in principle require only release ordering, not full volatile semantics, but are currently coded as volatile writes to be conservative.
I wonder if release ordering here means the happens-before relationship (an unlock of a monitor happens-before every subsequent lock of that same monitor) guaranteed by synchronized
. And if so, why setTabAt
is considered to be conservative, but not mandatory, given that the calls to tabAt
exist not only inside, but also outside synchronized
blocks? For example:
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
//...
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// -> tabAt called here, outside the synchronized block
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
// -> tabAt called here, inside the synchronized block
if (tabAt(tab, i) == f) {
// do insertion...
}
}
}
}
}
Another question is that in the code above, is the call to tabAt
inside the synchronized
block necessary? In my understanding, the monitor lock already takes care of the memory visibility between threads, for example:
tabAt
outside the synchronized
blocksetTabAt
)tabAt
(which in turn calls Unsafe.getObjectVolatile
) to access and re-check the element.Any help would be greatly appreciated.
In java-8, that method you mention is defined as:
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
In jdk-13, for example, it is already a release
:
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putReferenceRelease(tab, ((long)i << ASHIFT) + ABASE, v);
}
And, as far as I understand, is supposed to work in conjunction with :
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getReferenceAcquire(tab, ((long)i << ASHIFT) + ABASE);
}
So you can think as setTabAt
as set release
and tabAt
as get acquire
.
release
here means release/acquire semantics, which I sort of talked about in this answer. The point is that volatile
writes do "too much" for some cases (sequential consistency), like the one here.
There is comment in source code (of jdk-13) that says ( about putReferenceRelease
including) that this is a "weak(er) volatile":
Versions of putReferenceVolatile... that do not guarantee immediate visibility of the store to other threads...
The synchronized
part only gives memory visibility guarantees when the reading thread uses the same lock too; otherwise all bets are off. It seems this is the part that you are missing. Here is a more descriptive answer that explain how the synchronized
part can be badly broken.