简体   繁体   中英

Java 8 ConcurrentHashMap

I've observed that ConcurrentHashMap has been entirely rewritten in Java 8 to be more "lock-free". I've browsed the code of the get() method and see that there is no explicit lock mechanism:

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

Question:

How it is possible to see from one thread, modifications done to this hashmap from other threads, since the code isn't under a synchronize umbrella (which would enforce a happens-before relation)?

Note: The entire ConcurrentHashMap is a wrapper of a table: transient volatile Node<K,V>[] table;

So table is a volatile reference to an array, not a reference to an array of volatile elements! Which means that if someone is updating an element inside this array, the modification won't be seen in other threads.

Short answer

The Node#val is volatile which establishes your happens before ordering.

Longer answer

synchronized isn't a requirement for thread safety, it's one tool in a toolbox to make a system thread safe. You'll have to consider an entire set of actions on this ConcurrentHashMap to reason about thread safety.

It's useful to know the original ConcurrentHashMap too is non-blocking. Notice pre-Java 8 CHM get

V get(Object key, int hash) {
    if (count != 0) { // read-volatile
        HashEntry<K,V> e = getFirst(hash);
        while (e != null) {
            if (e.hash == hash && key.equals(e.key)) {
                V v = e.value;
                if (v != null)
                    return v;
                return readValueUnderLock(e); // ignore this
            }
            e = e.next;
        }
    }
    return null;
}

In this case, there is no blocking, so how does it work? The HashEntry#value is volatile . That is the synchronization point for thread safety.

The Node class for CHM-8 is the same.

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;

So a non-null val in this case should ensure thee happens-before relationship with respect to actions prior to a put.

The documentation does not state that synchronization occurs. For example it states

[...] aggregate operations such as putAll and clear , concurrent retrievals may reflect insertion or removal of only some entries.

In other words, there is a difference between allowing concurrent usage, and providing synchronized access.

The Java Language Specification writes :

If we have two actions x and y, we write hb(x, y) to indicate that x happens-before y.

  • If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).

  • There is a happens-before edge from the end of a constructor of an object to the start of a finalizer (§12.6) for that object.

  • If an action x synchronizes-with a following action y, then we also have hb(x, y).

  • If hb(x, y) and hb(y, z), then hb(x, z).

and defines

Synchronization actions induce the synchronized-with relation on actions, defined as follows:

  • An unlock action on monitor m synchronizes-with all subsequent lock actions on m (where "subsequent" is defined according to the synchronization order).

  • A write to a volatile variable v (§8.3.1.4) synchronizes-with all subsequent reads of v by any thread (where "subsequent" is defined according to the synchronization order).

  • An action that starts a thread synchronizes-with the first action in the thread it starts.

  • The write of the default value (zero, false, or null) to each variable synchronizes-with the first action in every thread.

    Although it may seem a little strange to write a default value to a variable before the object containing the variable is allocated, conceptually every object is created at the start of the program with its default initialized values.

  • The final action in a thread T1 synchronizes-with any action in another thread T2 that detects that T1 has terminated.

    T2 may accomplish this by calling T1.isAlive() or T1.join().

  • If thread T1 interrupts thread T2, the interrupt by T1 synchronizes-with any point where any other thread (including T2) determines that T2 has been interrupted (by having an InterruptedException thrown or by invoking Thread.interrupted or Thread.isInterrupted).

That is, reading a volatile field establishes happens-before just like an explicit lock.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM