I have been reading through Concurency in Practice
by Brian Goetz.
In the chapter about Lock Striping it is written that ConcurrentHashMap
uses 16 buckets to improve multithreaded access by many threads :
Lock splitting can sometimes be extended to partition locking on a variablesized set of independent objects, in which case it is called lock striping. For example, the implementation of ConcurrentHashMap uses an array of 16 locks, each of which guards 1/16 of the hash buckets; bucket N is guarded by lock N mod 16.
I have read those questions :
ConcurrentHashMap locking
Need simple explanation how “lock striping” works with ConcurrentHashMap
However those answers are valid for Java version <= 7.
For Java 8+ the behaviour seems to be changed significantly. For Java 8+ it seems that that the lock is acquired not for a Segment but for particular Node in table (transient volatile ConcurrentHashMap.Node<K, V>[] table;
). For example for the putVal
operation :
ConcurrentHashMap.Node var7;
.... ///retrive node for var7
synchronized(var7) {
....
}
And also from Java8 + field like DEFAULT_CONCURRENCY_LEVEL
and class Segment
seems to be unused in the implementation (it is only used in private method writeObject::ObjectOutputStream
and this method is not invoked anywhere in ConcurrentHashMap
implementation).
What is the cause of such significant change in ConcurrentHashMap
implementation?
If class Segment
is unused and also field like DEFAULT_CONCURRENCY_LEVEL
is also unused - why not get rid of it from implementation - is it for some historical reasons?
If we are not locking on segments, like it used to be for Java version < 7, is locking only on specific node sufficient? If yes - why? Does that mean that we do not need lock striping here?
Yes, ConcurrentHashMap uses a multitude of locks (by default, 16 of them), each lock controls one segment of the hash. When setting data in a particular segment, the lock for that segment is obtained. When getting data, a volatile read is used.
In ConcurrentHashMap, at a time any number of threads can perform retrieval operation but for updated in the object, the thread must lock the particular segment in which the thread wants to operate. This type of locking mechanism is known as Segment locking or bucket locking.
Lock striping is where the locking happens on a few containers or stripes or buckets, implying that getting to a pail just bolts that can and not the whole information structure. That is the idea of lock striping.
Regarding this: However, iterators are designed to be used by only one thread at a time. It means, while using iterators produced by ConcurrentHashMap in two threads are safe, it may cause an unexpected result in the application.
What is the cause of such significant change in ConcurrentHashMap implementation?
To reduce memory footprint (one of the reasons).
We do not want to waste the space required to associate a distinct lock object with each bin, so instead use the first node of a bin list itself as a lock. Locking support for these locks relies on builtin "synchronized" monitors.
openjdk > jdk8 >
java.util.concurrent.ConcurrentHashMap.java
> Line 314
If class
Segment
is unused and also field likeDEFAULT_CONCURRENCY_LEVEL
is also unused - why not get rid of it from implementation - is it for some historical reasons?
To ensure serialization compatibility.
We also declare an unused
Segment
class that is instantiated in minimal form only when serializing.openjdk > jdk8 >
java.util.concurrent.ConcurrentHashMap.java
> Line 486/** * The default concurrency level for this table. Unused but * defined for compatibility with previous versions of this class. */ private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
openjdk > jdk8 >
java.util.concurrent.ConcurrentHashMap.java
> Line 526/** * Stripped-down version of helper class used in previous version, * declared for the sake of serialization compatibility */ static class Segment<K,V> extends ReentrantLock implements Serializable { private static final long serialVersionUID = 2249069246763182397L; final float loadFactor; Segment(float lf) { this.loadFactor = lf; } }
openjdk > jdk8 >
java.util.concurrent.ConcurrentHashMap.java
> Line 1366
If we are not locking on segments, like it used to be for Java version < 7, is locking only on specific node sufficient?
No.
Using the first node of a list as a lock does not by itself suffice though: When a node is locked, any update must first validate that it is still the first node after locking it, and retry if not. Because new nodes are always appended to lists, once a node is first in a bin, it remains first until deleted or the bin becomes invalidated (upon resizing).
openjdk > jdk8 >
java.util.concurrent.ConcurrentHashMap.java
> Line 320
What is the cause of such significant change in
ConcurrentHashMap
implementation?
ConcurrentHashMap.java#l272:
* The primary design goal of this hash table is to maintain
* concurrent readability (typically method get(), but also
* iterators and related methods) while minimizing update
* contention.
Segment
class remains unused because of compatibility reasons, ConcurrentHashMap.java#l481:
* Maintaining API and serialization compatibility with previous
* versions of this class introduces several oddities. Mainly:
* [...]
* We also declare an unused "Segment" class that is
* instantiated in minimal form only when serializing.
... is locking only on specific node sufficient? If yes - why?
ConcurrentHashMap.java#l320:
* Using the first node of a list as a lock does not by itself
* suffice though: When a node is locked, any update must first
* validate that it is still the first node after locking it,
* and retry if not.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With