According to the documentation, some attempted update operations on this map by other threads may be blocked while computation is in progress, so the computation should be short and simple, and must not attempt to update any other mappings of this Map. It's not clear what is being blocked. Is it the access to the entire map or to the key? This article discusses that.
There is one ambiguous moment in the Java documentation of the compute()
method from the ConcurrentHashMap
class:
Some attempted update operations on this map by other threads may be blocked while computation is in progress, so the computation should be short and simple, and must not attempt to update any other mappings of this Map.
It's not clear what is being blocked. Is it the access to the entire map or to the key? Let's find out, by conducting a few experiments with the following code:
package tests.concurrency;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
public class TestConcurrentHashMapCompute implements Runnable {
private static final Map<key, String> MAP = new ConcurrentHashMap<>();
private static final int MAX_RUNS = 100;
private static final long SLEEP_TIME = 5000;
public static void main(String[] args) {
Thread th1 = new Thread(new TestConcurrentHashMapCompute());
Thread th2 = new Thread(new TestConcurrentHashMapCompute());
th1.start();
th2.start();
}
@Override
public void run() {
final String name = Thread.currentThread().getName();
for (int i = 0; i < MAX_RUNS; i++) {
final String value = MAP.compute(new Key(name), (k, v) -> {
System.out.println(name + ": enters");
sleep();
System.out.println(name + ": exits");
return name;
});
System.out.println(name + ": " + value);
}
}
private static void sleep() {
try {
Thread.sleep(SLEEP_TIME);
} catch (Exception ex) {}
}
private static class Key {
private final String value;
private Key(String value) {
this.value = value;
}
@Override
public int hashCode() {
return 1;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Key other = (Key) o;
return Objects.equals(value, other.value);
}
}
}
It's nothing complicated, two threads calling the compute()
method of a shared ConcurrentHashMap
instance, using Key
class instances as the key for the map. However, pay attention to the hashCode()
method inside the Key
class:
@Override
public int hashCode() {
return 1;
}
Depending on what is being returned (comment/uncomment the relevant lines), you will see different behaviour. And, by the way, return 1
is a valid hashCode()
implementation, according to the Java documentation:
- If two objects are equal according to the
equals()
method, then calling the hashCode method on each of the two objects must produce the same integer result. - It is not required that if two objects are unequal according to the
equals()
method, then calling the hashCode
method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hash tables.
The result is:
- With
return Objects.hash(value)
, i.e., String
's descent hashCode()
implementation, the threads run in parallel, as if the block was on the Key
- With
return 1
, the threads run sequentially, as if the block was on the entire Map
.
In fact, the block is on the bucket indexed by the Key
. That's what the hashCode()
method is about, to quickly access the relevant bucket. HashMap
, for example, uses a singly linked list as the storage for the buckets.