Search code examples
javaconcurrencyvolatileconcurrenthashmapjava-memory-model

java - volatile semantics in ConcurrentHashMap


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:

  1. Suppose that there's only one element in the bin, say Node-0
  2. Thread-A wants to insert Node-1 in the bin, right after Node-0 it founds by calling tabAt outside the synchronized block
  3. But before Thread-A can lock on Node-0, Thread-B locks Node-0 and deletes it (by calling setTabAt)
  4. Thread-A acquires the lock of Node-0 after Thread-B has released the lock
  5. As the happens-before relationship between Thread-A and Thread-B is guaranteed by the monitor lock, in this case, it seems to me that there's no need to call tabAt (which in turn calls Unsafe.getObjectVolatile) to access and re-check the element.

Any help would be greatly appreciated.


Solution

  • 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.