Detailed explanation of collection containers in Java: simple usage and case analysis

Table of contents

1. Overview

1.1 Collection

1. Set

2. List

3. Queue

1.2 Map

2. Design patterns in containers 

iterator pattern

adapter mode

3. Source code analysis

ArrayList

1. Overview

2. Expansion

3. Delete elements

4. Serialization

5. Fail-Fast

Vector

1. Synchronization

2. Expansion

3. Comparison with ArrayList

4. Alternatives

CopyOnWriteArrayList

1. Separation of reading and writing

2. Applicable scenarios

LinkedList

1. Overview

2. Comparison with ArrayList

HashMap

1. Storage structure

2. How the zipper method works

3. put operation

4. Determine the bucket index

5. Capacity expansion-basic principles

6. Comparison with Hashtable

7. Objects are stored as keys

ConcurrentHashMap

1. Storage structure

2. size operation

3. Changes in JDK 1.8

LinkedHashMap

storage structure

afterNodeAccess()

afterNodeInsertion()


       Java containers are a set of tools for storing data and objects. It can be compared to C++'s STL. Java containers are also called Java Collection Framework (JCF). In addition to containers that store objects, a set of utility classes are provided for processing and manipulating objects in the containers. Generally speaking, this is a framework that contains Java object containers and utility classes.

1. Overview

          Containers mainly include Collection and Map . Collection stores a collection of objects, while Map stores a mapping table of key-value pairs (two objects).

1.1 Collection

1. Set
  • TreeSet : Based on red-black tree implementation, supports ordered operations, such as searching for elements based on a range. However, the search efficiency is not as good as that of HashSet. The time complexity of HashSet search is O(1), while that of TreeSet is O(logN).
  • HashSet : Based on hash table implementation, supports fast search, but does not support ordered operations. And the insertion order information of the elements is lost, which means that the result obtained by using Iterator to traverse the HashSet is uncertain.
  • LinkedHashSet : It has the search efficiency of HashSet, and internally uses a doubly linked list to maintain the insertion order of elements.
2. List
  • ArrayList : Based on dynamic array implementation, supports random access.
  • Vector : Similar to ArrayList, but it is thread-safe.
  • LinkedList : Based on a doubly linked list, it can only be accessed sequentially, but it can quickly insert and delete elements in the middle of the linked list. Not only that, LinkedList can also be used as a stack, queue and deque.
3. Queue
  • LinkedList : You can use it to implement a two-way queue.
  • PriorityQueue : Based on the heap structure, you can use it to implement priority queues.

1.2 Map

  • TreeMap : implemented based on red-black trees.
  • HashMap : Based on hash table implementation.
  • HashTable : Similar to HashMap, but it is thread-safe, which means that multiple threads writing to HashTable at the same time will not cause data inconsistency. It is a legacy class and should not be used. Instead, use ConcurrentHashMap to support thread safety. ConcurrentHashMap will be more efficient because ConcurrentHashMap introduces segmentation locks.
  • LinkedHashMap : Use a doubly linked list to maintain the order of elements in insertion order or least recently used (LRU) order.

2. Design patterns in containers 

iterator pattern

Collection inherits the Iterable interface, in which the iterator() method can generate an Iterator object, through which the elements in the Collection can be iterated.

From JDK 1.5 onwards, you can use the foreach method to traverse aggregate objects that implement the Iterable interface.

List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
for (String item : list) {
    System.out.println(item);
}

adapter mode

java.util.Arrays#asList() can convert array type to List type.

@SafeVarargs
public static <T> List<T> asList(T... a)

It should be noted that the parameters of asList() are generic variable-length parameters. Basic type arrays cannot be used as parameters, and only corresponding packaging type arrays can be used.

Integer[] arr = {1, 2, 3};
List list = Arrays.asList(arr);

asList() can also be called using:

List list = Arrays.asList(1, 2, 3);

3. Source code analysis

Unless otherwise stated, the following source code analysis is based on JDK 1.8.

In IDEA, use double shift to call up Search EveryWhere, search for source code files, and then read the source code.

ArrayList

1. Overview

Because ArrayList is implemented based on arrays, it supports fast random access. The RandomAccess interface indicates that the class supports fast random access.

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

The default size of the array is 10.

private static final int DEFAULT_CAPACITY = 10;
2. Expansion

When adding elements, use the ensureCapacityInternal() method to ensure that the capacity is sufficient. If it is not enough, you need to use the grow() method to expand the capacity. The size of the new capacity is oldCapacity oldCapacity + (oldCapacity >> 1)+ oldCapacity/2 . Among them, oldCapacity >> 1 needs to be rounded, so the new capacity is about 1.5 times the old capacity. (If oldCapacity is an even number, it is 1.5 times, and if it is an odd number, it is 1.5 times-0.5)

The expansion operation requires calling to copy the entire original array to the new array. This operation is very expensive, so it is best to specify the approximate capacity when creating the ArrayList object to reduce the number of expansion operations.Arrays.copyOf()

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}
3. Delete elements

You need to call System.arraycopy() to copy all the elements after index+1 to the index position. The time complexity of this operation is O(N) . You can see that the cost of deleting elements in ArrayList is very high.

public E remove(int index) {
    rangeCheck(index);
    modCount++;
    E oldValue = elementData(index);
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    elementData[--size] = null; 
    return oldValue;
}
4. Serialization

ArrayList is implemented based on arrays and has dynamic expansion characteristics. Therefore, the arrays storing elements may not all be used, so there is no need to serialize them all.

The array elementData that holds the elements is modified with transient. This keyword declares that the array will not be serialized by default.

transient Object[] elementData;

ArrayList implements writeObject() and readObject() to control the serialization of only the portion of the array filled with elements.

private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;
    s.defaultReadObject();
    s.readInt(); 
    if (size > 0) {
        ensureCapacityInternal(size);
        Object[] a = elementData;
        for (int i=0; i<size; i++) {
            a[i] = s.readObject();
        }
    }
}
private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    int expectedModCount = modCount;
    s.defaultWriteObject();
    s.writeInt(size);
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

When serializing, you need to use writeObject() of ObjectOutputStream to convert the object into a byte stream and output it. The writeObject() method will reflect and call writeObject() of the object when the incoming object exists in writeObject() to achieve serialization. Deserialization uses the readObject() method of ObjectInputStream, and the principle is similar.

ArrayList list = new ArrayList();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(list);
5. Fail-Fast
The fail-fast mechanism is an error mechanism in Java collections . When multiple threads perform operations on the contents of the same collection
During operation, fail-fast events may occur .
For example: when a thread A traverses a collection through an iterator , if the content of the collection is changed by other threads
, then when thread A accesses the collection, a ConcurrentModificationException exception will be thrown, resulting in a fail-fast event .
pieces. The operations here mainly refer to add , remove and clear , which modify the number of collection elements.
Solution: It is recommended to use " classes under the java.util.concurrent package " to replace " classes under the java.util package " .
It can be understood this way: before traversing, write down modCount and expectModCount , and then go to expectModCount .
Compare with modCount. If they are not equal, it proves that it has been concurrent and modified, so it throws
ConcurrentModificationException exception (concurrent modification exception)

Vector

1. Synchronization

Its implementation is similar to ArrayList, but uses synchronized for synchronization.

public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}
public synchronized E get(int index) {
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);
    return elementData(index);
}
2. Expansion

The constructor of Vector can pass in the capacityIncrement parameter, which is used to increase the capacity by capacityIncrement during expansion. If the value of this parameter is less than or equal to 0, the capacity will be doubled each time during expansion.

public Vector(int initialCapacity, int capacityIncrement) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    this.elementData = new Object[initialCapacity];
    this.capacityIncrement = capacityIncrement;
}




private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                     capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}


//调用没有 capacityIncrement 的构造函数时,
capacityIncrement 值被设置为 0,也就是说默认情况下 Vector 每次扩容时容量都会翻倍。


public Vector(int initialCapacity) {
    this(initialCapacity, 0);
}
public Vector() {
    this(10);
}




3. Comparison with ArrayList
  • Vector is synchronized, so the overhead is greater than ArrayList and the access speed is slower. It is better to use ArrayList instead of Vector, because synchronization operations can be completely controlled by the programmer himself;
  • Vector requires 2 times its size each time it is expanded (the growing capacity can also be set through the constructor), while ArrayList requires 1.5 times.
4. Alternatives

You can use Collections.synchronizedList();to get a thread-safe ArrayList.

List<String> list = new ArrayList<>();
List<String> synList = Collections.synchronizedList(list);

You can also use the CopyOnWriteArrayList class under the concurrent package .

List<String> list = new CopyOnWriteArrayList<>();

CopyOnWriteArrayList

1. Separation of reading and writing

The writing operation is performed on a copied array, and the reading operation is still performed on the original array. Reading and writing are separated and do not affect each other.

Write operations need to be locked to prevent loss of written data due to concurrent writes.

After the write operation is completed, the original array needs to point to the new copied array.

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}
final void setArray(Object[] a) {
    array = a;
}
2. Applicable scenarios

CopyOnWriteArrayList allows reading operations at the same time as writing operations, which greatly improves the performance of reading operations, so it is very suitable for application scenarios with more reading and less writing.

But CopyOnWriteArrayList has its flaws:

  • Memory usage: When writing, a new array needs to be copied, causing the memory usage to be about twice the original size;
  • Data inconsistency: The read operation cannot read real-time data because some of the write operation data has not yet been synchronized to the reading group.

Therefore, CopyOnWriteArrayList is not suitable for memory-sensitive and real-time requirements scenarios.

LinkedList

1. Overview

Based on doubly linked list implementation, Node is used to store linked list node information.

private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;
}

每个链表存储了 first 和 last 指针:

transient Node<E> first;
transient Node<E> last;
2. Comparison with ArrayList

ArrayList is implemented based on dynamic arrays, and LinkedList is implemented based on doubly linked lists. The difference between ArrayList and LinkedList can be attributed to the difference between arrays and linked lists:

  • Arrays support random access, but insertion and deletion are expensive and require moving a large number of elements;
  • Linked lists do not support random access, but insertion and deletion only require changing the pointer.

HashMap

In order to facilitate understanding, the following source code analysis is mainly based on JDK 1.7.

1. Storage structure

It contains an array table of Entry type internally. Entry stores key-value pairs. It contains four fields. From the next field we can see that Entry is a linked list. That is, each position in the array is regarded as a bucket, and each bucket stores a linked list. HashMap uses the zipper method to resolve conflicts. Entries with the same hash value and hash bucket modulo operation result are stored in the same linked list.

transient Entry[] table;


static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    int hash;
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }
    public final K getKey() {
        return key;
    }
    public final V getValue() {
        return value;
    }
    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }
    public final boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry e = (Map.Entry)o;
        Object k1 = getKey();
        Object k2 = e.getKey();
        if (k1 == k2 || (k1 != null && k1.equals(k2))) {
            Object v1 = getValue();
            Object v2 = e.getValue();
            if (v1 == v2 || (v1 != null && v1.equals(v2)))
                return true;
        }
        return false;
    }
    public final int hashCode() {
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }
    public final String toString() {
        return getKey() + "=" + getValue();
    }
}
2. How the zipper method works
HashMap<String, String> map = new HashMap<>();
map.put("K1", "V1");
map.put("K2", "V2");
map.put("K3", "V3");
  • Create a new HashMap, the default size is 16;
  • Insert the <K1, V1> key-value pair, first calculate the hashCode of K1 to 115, and use the division-leaving-remainder method to get the bucket subscript 115%16=3.
  • Insert the <K2, V2> key-value pair, first calculate the hashCode of K2 to 118, and use the division-leaving-remainder method to get the bucket subscript 118%16=6.
  • Insert the <K3,V3> key-value pair, first calculate the hashCode of K3 to 118, use the division and remainder method to get the bucket subscript 118%16=6, and insert it in front of <K2,V2>.

It should be noted that the insertion of the linked list is carried out by head insertion. For example, the above <K3, V3> is not inserted after <K2, V2>, but at the head of the linked list.

The search needs to be divided into two steps:

  • Calculate the bucket where the key-value pair is located;
  • When searching sequentially on a linked list, the time complexity is obviously proportional to the length of the linked list.
3. put operation
public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    // 键为 null 单独处理
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    // 确定桶下标
    int i = indexFor(hash, table.length);
    // 先找出是否已经存在键为 key 的键值对,如果存在的话就更新这个键值对的值为 value
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    // 插入新键值对
    addEntry(hash, key, value, i);
    return null;
}

 HashMap allows inserting key-value pairs with null keys. However, because the hashCode() method of null cannot be called, the bucket index of the key-value pair cannot be determined, and it can only be stored by forcibly specifying a bucket index. HashMap uses the 0th bucket to store key-value pairs with null keys.

private V putForNullKey(V value) {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);
    return null;
}

Use the head insertion method of the linked list, that is, the new key-value pair is inserted at the head of the linked list, not the tail of the linked list.

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
    createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    // 头插法,链表头部指向新的键值对
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}




Entry(int h, K k, V v, Entry<K,V> n) {
    value = v;
    next = n;
    key = k;
    hash = h;
}
4. Determine the bucket index

Many operations require first determining the bucket index where a key-value pair is located.

int hash = hash(key);
int i = indexFor(hash, table.length);

4.1 Calculate hash value

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }
    h ^= k.hashCode();
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}


public final int hashCode() {
    return Objects.hashCode(key) ^ Objects.hashCode(value);
}

4.2 Modeling

Let x = 1<<4, that is, x is the 4th power of 2, which has the following properties:

x   : 00010000
x-1 : 00001111



令一个数 y 与 x-1 做与运算,可以去除 y 位级表示的第 4 位以上数:
y       : 10110010
x-1     : 00001111
y&(x-1) : 00000010


这个性质和 y 对 x 取模效果是一样的:

y   : 10110010
x   : 00010000
y%x : 00000010


我们知道,位运算的代价比求模运算小的多,因此在进行这种计算时用位运算的话能带来更高的性能。

确定桶下标的最后一步是将 key 的 hash 值对桶个数取模:hash%capacity,如果能保证 capacity 为 2 的 n 次方,那么就可以将这个操作转换为位运

static int indexFor(int h, int length) {
    return h & (length-1);
}
5. Capacity expansion-basic principles

Assume the table length of HashMap is M, and the number of key-value pairs that need to be stored is N. If the hash function meets the uniformity requirements, then the length of each linked list is approximately N/M, so the search complexity is O(N/ M).

In order to reduce the search cost, N/M should be made as small as possible, so M needs to be as large as possible, that is to say, the table should be as large as possible. HashMap uses dynamic expansion to adjust the M value according to the current N value, so that both space efficiency and time efficiency can be guaranteed.

The parameters related to expansion mainly include: capacity, size, threshold and load_factor.

parameter meaning
capacity The capacity of the table, the default is 16. It should be noted that capacity must be guaranteed to be 2 to the nth power.
size Number of key-value pairs.
threshold The critical value of size. When size is greater than or equal to threshold, expansion operation must be performed.
loadFactor Load factor, the proportion that the table can use, threshold = (int)(capacity* loadFactor).
static final int DEFAULT_INITIAL_CAPACITY = 16;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
transient Entry[] table;
transient int size;
int threshold;
final float loadFactor;
transient int modCount;

 As can be seen from the code for adding elements below, when expansion is required, the capacity is doubled.

void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    if (size++ >= threshold)
        resize(2 * table.length);
}

Expansion is implemented using resize(). It should be noted that the expansion operation also requires re-inserting all the key-value pairs of the oldTable into the newTable, so this step is very time-consuming.

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    Entry[] newTable = new Entry[newCapacity];
    transfer(newTable);
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);
}
void transfer(Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}
6. Comparison with Hashtable
  • Hashtable uses synchronized for synchronization.
  • HashMap can insert Entries with null keys.
  • HashMap's iterator is a fail-fast iterator.
  • HashMap cannot guarantee that the order of elements in the Map will remain unchanged over time.
7. Objects are stored as keys
  • Override the hashCode() and equals() methods to ensure that the Map can operate and retrieve objects correctly.
  • Ensure object immutability to avoid modifying the object's state after it has been used as a key.
  • Optionally implements the Comparable interface to support sorting of keys.
  • Well-designed hashCode() method to reduce the possibility of hash collisions.
  • Avoid using mutable objects as keys and update the keys in the Map promptly when necessary.
     

ConcurrentHashMap

1. Storage structure
static final class HashEntry<K,V> {
    final int hash;
    final K key;
    volatile V value;
    volatile HashEntry<K,V> next;
}

ConcurrentHashMap and HashMap are similar in implementation. The main difference is that ConcurrentHashMap uses segment locks (Segment). Each segment lock maintains several buckets (HashEntry). Multiple threads can access buckets on different segment locks at the same time. This makes the concurrency higher (the concurrency is the number of Segments).

Segment inherits from ReentrantLock.

static final class Segment<K,V> extends ReentrantLock implements Serializable {
    private static final long serialVersionUID = 2249069246763182397L;
    static final int MAX_SCAN_RETRIES =
        Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
    transient volatile HashEntry<K,V>[] table;
    transient int count;
    transient int modCount;
    transient int threshold;
    final float loadFactor;
}


final Segment<K,V>[] segments;

默认的并发级别为 16,也就是说默认创建 16 个 Segment。

static final int DEFAULT_CONCURRENCY_LEVEL = 16;
2. size operation

Each Segment maintains a count variable to count the number of key-value pairs in the Segment.

transient int count;

When performing the size operation, it is necessary to traverse all Segments and then accumulate the count.

ConcurrentHashMap first tries not to lock when performing the size operation. If the results obtained by two consecutive non-locking operations are consistent, the result can be considered correct.

The number of attempts is defined using RETRIES_BEFORE_LOCK, which has a value of 2. The initial value of retries is -1, so the number of attempts is 3.

If the number of attempts exceeds 3, each Segment needs to be locked.

static final int RETRIES_BEFORE_LOCK = 2;
public int size() {
    final Segment<K,V>[] segments = this.segments;
    int size;
    boolean overflow; 
    long sum;        
    long last = 0L; 
    int retries = -1;
    try {
        for (;;) {
            // 超过尝试次数,则对每个 Segment 加锁
            if (retries++ == RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    ensureSegment(j).lock(); 
            }
            sum = 0L;
            size = 0;
            overflow = false;
            for (int j = 0; j < segments.length; ++j) {
                Segment<K,V> seg = segmentAt(segments, j);
                if (seg != null) {
                    sum += seg.modCount;
                    int c = seg.count;
                    if (c < 0 || (size += c) < 0)
                        overflow = true;
                }
            }
            // 连续两次得到的结果一致,则认为这个结果是正确的
            if (sum == last)
                break;
            last = sum;
        }
    } finally {
        if (retries > RETRIES_BEFORE_LOCK) {
            for (int j = 0; j < segments.length; ++j)
                segmentAt(segments, j).unlock();
        }
    }
    return overflow ? Integer.MAX_VALUE : size;
}
3. Changes in JDK 1.8

JDK 1.7 uses the segment lock mechanism to implement concurrent update operations. The core class is Segment, which inherits from the reentrant lock ReentrantLock. The degree of concurrency is equal to the number of Segments.

JDK 1.8 uses CAS operations to support higher concurrency, and uses built-in lock synchronized when CAS operations fail.

And the implementation of JDK 1.8 will also convert to a red-black tree when the linked list is too long.

LinkedHashMap

storage structure

Inherited from HashMap, it has the same fast search characteristics as HashMap.

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>

A doubly linked list is maintained internally to maintain the insertion order or LRU order.

transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;

accessOrder determines the order, and the default is false. At this time, the insertion order is maintained.

final boolean accessOrder;

The most important thing about LinkedHashMap is the following functions for maintaining order, which will be called in put, get and other methods.

void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
afterNodeAccess()

When a node is accessed, if accessOrder is true, the node will be moved to the end of the linked list. That is to say, after specifying the LRU order, each time a node is accessed, the node will be moved to the end of the linked list to ensure that the end of the linked list is the most recently visited node, and the head of the linked list is the most recent and longest unused node.

void afterNodeAccess(Node<K,V> e) { 
    LinkedHashMap.Entry<K,V> last;
    if (accessOrder && (last = tail) != e) {
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a != null)
            a.before = b;
        else
            last = b;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}
afterNodeInsertion()

It is executed after operations such as put. When the removeEldestEntry() method returns true, the latest node, which is the first node of the linked list, will be removed.

evict is false only when building the Map, here it is true.

void afterNodeInsertion(boolean evict) { 
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

removeEldestEntry() 默认为 false,如果需要让它为 true,需要继承 LinkedHashMap 并且覆盖这个方法的实现,这在实现 LRU 的缓存中特别有用,通过移除最近最久未使用的节点,从而保证缓存空间足够,并且缓存的数据都是热点数据。

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}

Guess you like

Origin blog.csdn.net/XikYu/article/details/132041595