Concurrent HashMap for java Concurrent Programming Learning (JDK 1.7)

Posted by burntheblobs on Mon, 29 Jul 2019 11:42:11 +0200

before HashMap As mentioned above, concurrency can cause a dead loop, which can be avoided by using Concurrent HashMap in multithreading.

Segment

Concurrent HashMap is composed of several Segments. Segments inherit ReentrantLock. Each lock is to one Segment without affecting other Segments, and achieves the function of lock separation (also called segmented lock).
Each Segment also contains an array of HashEntry, which is a linked list. As shown in the following figure:

Initialization

Initial Capacity: Initial capacity size, default 16.
Load Factor: Expansion factor, table expansion use, Segments does not expand. By default, 0.75, when the Segment capacity is greater than the initial Capacity * loadFactor, the expansion begins.
Concurrency Level: Concurrent number, default 16, directly affects the value of segmentShift and segmentMask, and the number of segments initialized. The number of Segments initialized is the nearest and greater value to the N power of 2, such as concurrencyLevel=16, Segments 16, concurrencyLevel=17, Segments 32. SegmentShift's value is like this, for example, Segment is 32, relative to the 5th power of 2, then its value is 32-5, 27, followed by unsigned right-shift 27 bits, that is, when 5 bits higher, it is 0-31 value, at this time, the subscript of Segment is also 0-31, which corresponds to each Segment after taking the module. SegmentMask is the n-th power-1 of 2, where n is 5 for modulus. The indexFor method of hashmap has been mentioned before.
When initializing, you also initialize the first Segment and the size of the table array in the Segment, which is greater than or equal to the number of initial Capacity divided by the Segment array, with an average allocation of 2 and the N th power of 2. For example, when initial Capacity is 32 and concurrency Level is 16, then the number of Segments is 16, 32 divided by 16, which is equal to 2. If initial Capacity is 33, Segments are 16, 33 divided by 16, 4.

public ConcurrentHashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
    // Find power-of-two sizes best matching arguments
    int sshift = 0;
    int ssize = 1;
    while (ssize < concurrencyLevel) {
        ++sshift;
        ssize <<= 1;
    }
    this.segmentShift = 32 - sshift;//Used in high position to determine which Segment to fall on
    this.segmentMask = ssize - 1;//Used to take the mould. The indexFor method of hashmap has been mentioned before. n-1 of 2
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    int c = initialCapacity / ssize;
    if (c * ssize < initialCapacity)
        ++c;
    int cap = MIN_SEGMENT_TABLE_CAPACITY;
    while (cap < c)
        cap <<= 1;
    // create segments and segments[0]
    Segment<K,V> s0 =
        new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                         (HashEntry<K,V>[])new HashEntry[cap]);//Initialize the Segment of the first location
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];//Initialize Segments
    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    this.segments = ss;
}

put method

public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    int hash = hash(key);
    //Move right unsigned and take the module. Which Segment does it fall on?
    int j = (hash >>> segmentShift) & segmentMask;
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j);
    return s.put(key, hash, value, false);
}

ensureSegment method
Determine which Segment to fall on and initialize if it's empty, because the first Segment was initialized before.

private Segment<K,V> ensureSegment(int k) {
    final Segment<K,V>[] ss = this.segments;
    long u = (k << SSHIFT) + SBASE; // raw offset
    Segment<K,V> seg;
    if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
        //Initialization using segment[0] table length and loadFactor
        Segment<K,V> proto = ss[0]; // use segment 0 as prototype
        int cap = proto.table.length;
        float lf = proto.loadFactor;
        int threshold = (int)(cap * lf);
        HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
            == null) { // recheck
            Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
            while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))//cas operation, only one set value succeeds, if other succeeds, assign value, and return
                   == null) {
                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                    break;
            }
        }
    }
    return seg;
}

put method

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    HashEntry<K,V> node = tryLock() ? null :
        scanAndLockForPut(key, hash, value);//Getting Segment Locks
    V oldValue;
    try {
        HashEntry<K,V>[] tab = table;
        int index = (tab.length - 1) & hash;//Above is the hash to get the high position of the Segment, and here is the hash of the tabel.
        HashEntry<K,V> first = entryAt(tab, index);//Headers of arrays fetched to hash positions
        for (HashEntry<K,V> e = first;;) {//Traversing from the beginning to the end
            if (e != null) {
                K k;
                if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {//The key is the same, or the hash value is the same
                    oldValue = e.value;
                    if (!onlyIfAbsent) {//Replacement or not
                        e.value = value;
                        ++modCount;
                    }
                    break;
                }
                e = e.next;
            }
            else {
                if (node != null)//Not empty, set to header
                    node.setNext(first);
                else
                    node = new HashEntry<K,V>(hash, key, value, first);/Place the header after initialization
                int c = count + 1;
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node);// Capacity expansion
                else
                    setEntryAt(tab, index, node);//Place the new node on tab index
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
        unlock();//Release lock
    }
    return oldValue;
}

Scan AndLockForPut Method
Attempt to acquire locks without first initializing node s

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
    HashEntry<K,V> first = entryForHash(this, hash);//There is null in the header node after hash
    HashEntry<K,V> e = first;
    HashEntry<K,V> node = null;
    int retries = -1; // negative while locating node
    while (!tryLock()) {//This put method first tries to get, but it can't. Here, while loop tries to get.
        HashEntry<K,V> f; // to recheck first below
        if (retries < 0) {
            if (e == null) {//When the node is empty
                if (node == null) // speculatively create node
                    node = new HashEntry<K,V>(hash, key, value, null);//Initialize node
                retries = 0;
            }
            else if (key.equals(e.key))//When the head node is not empty
                retries = 0;
            else
                e = e.next;
        }
        else if (++retries > MAX_SCAN_RETRIES) {//Over the number of retries, enter the blocking queue and wait for the lock
            lock();
            break;
        }
        else if ((retries & 1) == 0 &&
                 (f = entryForHash(this, hash)) != first) {//It's not first, it's already got other nodes in.
            e = first = f; // re-traverse if entry changed
            retries = -1;
        }
    }
    return node;
}

rehash method, expansion, table expansion

private void rehash(HashEntry<K,V> node) {
    HashEntry<K,V>[] oldTable = table;
    int oldCapacity = oldTable.length;
    int newCapacity = oldCapacity << 1;//Move left, twice as much as before.
    threshold = (int)(newCapacity * loadFactor);
    HashEntry<K,V>[] newTable =
        (HashEntry<K,V>[]) new HashEntry[newCapacity];
    int sizeMask = newCapacity - 1;
    for (int i = 0; i < oldCapacity ; i++) {
        HashEntry<K,V> e = oldTable[i];
        if (e != null) {
            HashEntry<K,V> next = e.next;
            int idx = e.hash & sizeMask;
            if (next == null)   //  Empty, no later nodes, directly to the new array
                newTable[idx] = e;
            else { // Reuse consecutive sequence at same slot
                HashEntry<K,V> lastRun = e;
                int lastIdx = idx;
                //Because the array is twice as large as the previous index, after hash, it either falls in the same position as the previous index or adds the value of old Capacity.
                //For example, capacity is 2, expansion 4, hash is 2, 4, 6, 10, 14, then the latter three are divided by 4 more than 2, can be directly copied.
                for (HashEntry<K,V> last = next;
                     last != null;
                     last = last.next) {
                    int k = last.hash & sizeMask;
                    if (k != lastIdx) {//hash is different, again
                        lastIdx = k;
                        lastRun = last;
                    }
                }
                //To execute the above, lastRun is 6, 10, 14
                newTable[lastIdx] = lastRun;//Above
                // Clone remaining nodes when cloning, encounter lastrun, directly based on the so-called value, but the previous possible index is the same as lastrun, such as 2
                for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                    V v = p.value;
                    int h = p.hash;
                    int k = h & sizeMask;
                    HashEntry<K,V> n = newTable[k];
                    newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
                }
            }
        }
    }
    int nodeIndex = node.hash & sizeMask; // add the new node
    node.setNext(newTable[nodeIndex]);//Join the End Node
    newTable[nodeIndex] = node;
    table = newTable;
}

get method

public V get(Object key) {
    Segment<K,V> s; // manually integrate access methods to reduce overhead
    HashEntry<K,V>[] tab;
    int h = hash(key);
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&//Find Segment, logical same as put
        (tab = s.table) != null) {
        for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                 (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);//Find the table, logic and put
             e != null; e = e.next) {//Traversal table
            K k;
            if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                return e.value;
        }
    }
    return null;
}

Topics: Java