【Java集合】源码分析

如果没有特别声明,以下源码分析基于JDK8。

ArrayList

1. 概述

应为ArrayList是基于数组实现的,所以支持快速随机访问。RandomAccess接口标识着该类支持快速随机访问。

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

数组的默认大小为10。

private static final int DEFAULT_CAPACITY = 10;

elementData

2. 扩容

添加元素时使用ensureCapacityInternal方法来保证容量足够,如果不够时,需要使用grow方法进行扩容,新容量的大小为newCapacity = oldCapacity + (oldCapacity >> 1);,也就是旧容量的1.5倍。

扩容操作需要调用Arrays.copyOf()方法,把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建ArrayList对象时就指定大概的容量大小,减少扩容操作的次数。

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

3. 删除元素

需要调用System.arraycopy() 将index+1后面的元素都复制到index位置上,该操作的时间复杂度为O(N),可以看出ArrayList删除元素的代价是非常高的。

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; // clear to let GC do its work

    return oldValue;
}

4. Fail-Fast

扫描二维码关注公众号,回复: 6163589 查看本文章

modCount用来记录ArrayList结构发生变化的次数。结构发生变化是指添加或删除至少一个元素的所有的操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。

在进行序列化或者迭代等操作时,需要比较操作前后modCount是否改变,如果改变了需要抛出ConcurrentModificationException

private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;

    public boolean hasNext() {
        return cursor != size;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        checkForComodification();
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}
private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    s.defaultWriteObject();

    // Write out size as capacity for behavioural compatibility with clone()
    s.writeInt(size);

    // Write out all elements in the proper order.
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }

    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

5. 序列化

ArrayList基于数组实现,并且具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。

保存元素的数组elementData使用transient修饰,该关键字声明数组默认不会被序列化。

private transient Object[] elementData;

ArrayList实现了writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。

private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    s.defaultWriteObject();

    // Write out size as capacity for behavioural compatibility with clone()
    s.writeInt(size);

    // Write out all elements in the proper order.
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }

    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}
private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;

    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in capacity
    s.readInt(); // ignored

    if (size > 0) {
        // be like clone(), allocate array based upon size not capacity
        ensureCapacityInternal(size);

        Object[] a = elementData;
        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            a[i] = s.readObject();
        }
    }
}

序列化时需要使用ObjectOutputStream的writeObject() 将对象转换为字节流并输出。而writeObject() 方法在传入的对象存在writeObject() 的时候会去反射调用该对象的writeObject() 来实现序列化。反序列化使用的是ObjectInputStream的readObject() 方法,原理类似。

Vector

1. 同步

它的实现与ArrayList类似,但是使用了synchronized进行同步

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. 与ArrayList的比较

  • Vector是同步的,因此开销就比ArrayList要大,访问速度更慢。
  • Vector每次扩容请求其大小的2倍空间,而ArrayList是1.5倍。

3. 替代方案

可以使用Collections.synchronizedList();得到一个线程安全的ArrayList

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

CopyOnWriteArrayList

读写分离

写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。

写操作需要加锁,防止并发写入时导致写入数据丢失。

写操作结束之后需要把原始数组指向新的复制数组。

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;
}
public E get(int index) {
    return get(getArray(), index);
}

final Object[] getArray() {
    return array;
}

适用场景

CopyOnWriteArrayList在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。

但是CopyOnWriteArrayList有其缺陷:

  • 内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右
  • 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中

所以CopyOnWriteArrayList不适合内存敏感以及对实时性要求很高的场景。

LinkedList

1. 概述

基于双向链表实现,使用Node存储链表节点信息

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

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

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

transient Node<E> first;
transient Node<E> last;

LinkedList-Node
2. 与ArrayList的比较

  • ArrayList基于动态数组实现,LinkedList基于双向链表实现
  • ArrayList支持随机访问,LinkedList不支持
  • LinkedList在任意位置添加和删除元素更快。

HashMap

1. 存储结构

内部包含了一个Node类型的数组table。

transient Node<K,V>[] table;

Node存储着键值对。它包含了四个字段,从next字段我们可以看出Node是一个链表。即数组中的每个位置被当成一个桶,一个桶存放一个链表。HashMap使用拉链法来解决冲突,同一个链表中存放哈希值相同的Node。

HashMap-table

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

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

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

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

2. put 操作

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

HashMap允许插入键为null的键值对,null的hash为0,也就是会放在第0个桶中。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

使用链表的尾插法,也就是新的键值对插在链表的尾部,而不是链表的头部。

for (int binCount = 0; ; ++binCount) {
    if ((e = p.next) == null) {
        p.next = newNode(hash, key, value, null); // 尾插法,放在尾部
        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
            treeifyBin(tab, hash);
        break;
    }
    if (e.hash == hash &&
        ((k = e.key) == key || (key != null && key.equals(k))))
        break;
    p = e;
}

3. 确定桶下标

很多操作都需要先确定一个键值对所在的桶下标

3.1 计算 hash 值

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

3.2 取模

令 x = 1<<4,即 x 为 2 的 4 次方,它具有以下性质:

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次方,那么就可以将这个操作转换为为运算。

i = (n - 1) & hash

4. 扩容

设HashMap的table长度为M,需要存储的键值对数量为N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为N/M,因此平均查找次数的复杂度为O(N/M)。

为了让查找的成本降低,应该尽可能使得N/M尽可能小,因此需要保持M尽可能大,也就是说table要尽可能大。
HashMap采用动态扩容来根据当前的N值来调整M值,使得空间效率和时间效率都能得到保证。

和扩容相关的参数主要有:capacity、size、threshold和loadFactor

参数 含义
capacity table的容量大小,默认为16.需要注意的是capacity必须保证为2的n次方。
size 键值对数量
threshold size的临界值,当size大于threshold就必须进行扩容操作
loadFactor 装载因子,table能够使用的比例,threshold = capacity * loadFactor
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

static final int MAXIMUM_CAPACITY = 1 << 30;

static final float DEFAULT_LOAD_FACTOR = 0.75f;

transient Node<K,V>[] table;

transient int size;

int threshold;

final float loadFactor;

当需要扩容时,capacity为原来的2倍

int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
    if (oldCap >= MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return oldTab;
    }
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
             oldCap >= DEFAULT_INITIAL_CAPACITY)
        newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
    newCap = oldThr;
else {               // zero initial threshold signifies using defaults
    newCap = DEFAULT_INITIAL_CAPACITY;
    newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}

if (newThr == 0) {
    float ft = (float)newCap * loadFactor;
    newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
              (int)ft : Integer.MAX_VALUE);
}

当capacity从16扩容到32时,hash值为5和21的情况是如何变化的呢

扩容前如下:

101   & 111   = 101 = 5
10101 & 01111 = 101 = 5

扩容后如下:

101   & 111   = 101   = 5
10101 & 11111 = 10101 = 21

可以发现,把hash值与原来capacity值相与,如果结果为0,那么桶位置不变;如果结果为1,那么桶位置为“原位置+旧capacity”。HashMap-resize

Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;

do {
    next = e.next;
    if ((e.hash & oldCap) == 0) { // 与旧容量相与
        if (loTail == null)
            loHead = e;
        else
            loTail.next = e;
        loTail = e;
    }
    else {
        if (hiTail == null)
            hiHead = e;
        else
            hiTail.next = e;
        hiTail = e;
    }
} while ((e = next) != null);

if (loTail != null) {
    loTail.next = null;
    newTab[j] = loHead;
}
if (hiTail != null) {
    hiTail.next = null;
    newTab[j + oldCap] = hiHead;
}

5. 计算数组容量

HashMap 构造函数允许用户传入的容量不是2的n次方,因为它可以自动地将传入的容量转换为2的n次方。

先考虑如何求一个数的掩码,对于10010000,它的掩码为11111111,可以使用以下方法得到:

mask |= mask >> 1	11011000
mask |= mask >> 2	11111110
mask |= mask >> 4	11111111

mask + 1 是大于原始数字的最小的2的n次方

num     10010000
mask+1 100000000

以下是HashMap中计算数组容量的代码:

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

6. 链表转为红黑树

当一个桶中至少有TREEIFY_THRESHOLD个节点,并且table的capacity大于等于MIN_TREEIFY_CAPACITY,才会将这个桶中的链表转化为红黑树,否则扩容。

// 当节点数大于等于此阈值时,普通链表转为红黑树(前提是capacity大于等于MIN_TREEIFY_CAPACITY)
static final int TREEIFY_THRESHOLD = 8;

// 当节点数小于等于此阈值时,红黑树转化为普通链表
static final int UNTREEIFY_THRESHOLD = 6;

// 只有当capacity大于等于此阈值时,才会真正的把普通链表转化为红黑树(即使节点数大于等于TREEIFY_THRESHOLD)
// 否则只是扩容
static final int MIN_TREEIFY_CAPACITY = 64;
for (int binCount = 0; ; ++binCount) {
    if ((e = p.next) == null) {
        p.next = newNode(hash, key, value, null);
        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
            treeifyBin(tab, hash);
        break;
    }
    if (e.hash == hash &&
        ((k = e.key) == key || (key != null && key.equals(k))))
        break;
    p = e;
}
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

7. 与HashTable的比较

  • HashTable 使用 synchronized 来进行同步
  • HashMap允许插入键为null的节点,HashTable只能插入key不为null并且value也为null的节点

8. 与JDK7的HashMap的比较

  • JDK7用的是头插法,JDK8采用的是尾插法
  • 扩容时存储位置的计算方式不一样:JDK7是直接用hash值与新容量进行按位与;而JDK8采用hash值与旧容量按位与得到的值,判断是在原来桶位置,还是原来桶位置+旧容量
  • JDK7采用数组+链表的数据结构;而JDK8采用的是数组+链表+红黑树的数据结构。

LinkedHashMap

存储结构

继承自HashMap,因此具有和HashMap一样的快速查找特性。

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

内部维护了一个双向链表,用来维护插入顺序或LRU顺序。

/**
 * The head (eldest) of the doubly linked list.
 */
transient LinkedHashMap.Entry<K,V> head;

/**
 * The tail (youngest) of the doubly linked list.
 */
transient LinkedHashMap.Entry<K,V> tail;

accessOrder决定了顺序,默认为false,此时维护的是插入顺序。

/**
 * The iteration ordering method for this linked hash map: <tt>true</tt>
 * for access-order, <tt>false</tt> for insertion-order.
 *
 * @serial
 */
final boolean accessOrder;

LinkedHashMap最重要的是以下维护顺序的函数,它们会在put、get等方法调用。

void afterNodeAccess(Node<K,V> e) { }

void afterNodeInsertion(boolean evict) { }

afterNodeAccess()

当一个节点被访问后,如果accessOrder为true,则会将该节点移到链表尾部。也就是说LRU顺序之后,每次访问一个节点时,会将这个节点移到链表尾部,保证链表尾部是最近访问的节点。那么链表首部就是最近最久未使用的节点。

void afterNodeAccess(Node<K,V> e) { // move node to last
    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()

在put等操作之后执行,当removeEldestEntry() 方法返回true时会移出最晚的节点,也就是链表首部节点first。

evict只有在构建Map的时候才为false,在这里为true。

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    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;
}

LRU 缓存

以下是使用LinkedHashMap实现的一个LRU缓存:

  • 设定最大缓存空间MAX_ENTRIES 为 3
  • 使用LinkedHashMap的构造函数将accessOrder设置为true,开启LRU顺序
  • 覆盖removeEldestEntry() 方法实现,在节点多于MAX_ENTRIES就会将最近最久未使用的数据移出。
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
	private static final long serialVersionUID = 1L;

	private static final int MAX_ENTRIES = 3;

	@Override
	protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
		return size() > MAX_ENTRIES;
	}

	public LRUCache() {
		super(MAX_ENTRIES, 0.75f, true);
	}

	public static void main(String[] args) {
		LRUCache<Integer, String> cache = new LRUCache<>();
		cache.put(1, "a");
		cache.put(2, "b");
		cache.put(3, "c");
		cache.get(1);
		cache.put(4, "d");
		System.out.println(cache.keySet()); // [3, 1, 4]
	}
}

ConcurrentHashMap

1. JDK7 实现

1.1 数据结构

JDK7中采用Segment + HashEntry的方式进行实现

final Segment<K,V>[] segments;
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;
}
static final class HashEntry<K,V> {
    final int hash;
    final K key;
    volatile V value;
    volatile HashEntry<K,V> next;
}

ConcurrentHashMap-segment
Segment在实现上继承了ReentrantLock,这样就自带了锁的功能。

1.2 put实现

必须插入key不为null,并且value也不为null。

当执行put方法插入数据时,根据key 的hash值,在Segment数组中找到相应的位置,如果相应位置的Segment还未初始化,则通过CAS进行赋值,接着执行Segment对象的put方法通过加锁机制插入数据。

public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    int hash = hash(key);
    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);
}
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) {
        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))
                   == null) {
                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                    break;
            }
        }
    }
    return seg;
}

1.3 size实现

每个Segment维护了一个count变量来统计该Segment中的键值对个数。

/**
 * The number of elements. Accessed only either within locks
 * or among other volatile reads that maintain visibility.
 */
transient int count;

在执行size操作时,需要遍历所有Segment然后把count累计起来。

ConcurrentHashMap在执行size操作时先尝试不加锁,如果连续两次不加锁操作得到的结果一致,那么可以认为这个结果是正确的。

尝试次数使用RETRIES_BEFORE_LOCK定义,该值为2,retries初始值为-1,因此尝试次数为3。

如果尝试的次数超过3次,就需要对每个Segment加锁。

static final int RETRIES_BEFORE_LOCK = 2;

public int size() {
    // Try a few times to get accurate count. On failure due to
    // continuous async changes in table, resort to locking.
    final Segment<K,V>[] segments = this.segments;
    int size;
    boolean overflow; // true if size overflows 32 bits
    long sum;         // sum of modCounts
    long last = 0L;   // previous sum
    int retries = -1; // first iteration isn't retry
    try {
        for (;;) {
            if (retries++ == RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    ensureSegment(j).lock(); // force creation
            }
            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;
}

2. JDK8实现

2.1 数据结构

词用Node + CAS + Synchronized来保证并发安全进行实现

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

HashMap-table

2.2 put实现

必须插入key不为null,并且value也不为null。

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
	......
}

当执行put方法插入数据时,根据key的hash值,在Node数组中找到相应的位置。

如果相应位置的Node还未初始化,则通过CAS插入相应的数据

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
}

如果相应位置的Node不为空,且当前该节点不处于移动状态,则对该节点加synchronized锁,如果该节点的hash不小于0,则遍历链表更新节点或插入新节点,如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点。

synchronized (f) {
    if (tabAt(tab, i) == f) {
        if (fh >= 0) {
            binCount = 1;
            for (Node<K,V> e = f;; ++binCount) {
                K ek;
                if (e.hash == hash &&
                    ((ek = e.key) == key ||
                     (ek != null && key.equals(ek)))) {
                    oldVal = e.val;
                    if (!onlyIfAbsent)
                        e.val = value;
                    break;
                }
                Node<K,V> pred = e;
                if ((e = e.next) == null) {
                    pred.next = new Node<K,V>(hash, key,
                                              value, null);
                    break;
                }
            }
        }
        else if (f instanceof TreeBin) {
            Node<K,V> p;
            binCount = 2;
            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                           value)) != null) {
                oldVal = p.val;
                if (!onlyIfAbsent)
                    p.val = value;
            }
        }
    }
}

2.3 size实现

JDK8中使用一个volatile类型的变量baseCount记录元素的个数,当插入新数据或删除数据时,会通过addCount()方法更新baseCount。

private transient volatile long baseCount;
private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell a; long v; int m;
        boolean uncontended = true;
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended =
              U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
            fullAddCount(x, uncontended);
            return;
        }
        if (check <= 1)
            return;
        s = sumCount();
    }
    if (check >= 0) {
        Node<K,V>[] tab, nt; int n, sc;
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {
            int rs = resizeStamp(n);
            if (sc < 0) {
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
            s = sumCount();
        }
    }
}

初始化时counterCells为空,在并发量很高时,如果存在两个线程同时执行CAS修改baseCount值,则失败的线程会继续执行方法体中的逻辑,使用counterCells记录元素个数的变化。

所以在JDK8,元素个数保存到baseCount中,部分元素的变化个数保存在CounterCell数组中。

通过累加baseCount和CounterCell数组中的数量,即可得到元素的总个数。

public int size() {
    long n = sumCount();
    return ((n < 0L) ? 0 :
            (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
            (int)n);
}
final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

WeakHashMap

存储结构

WeakHashMap的Entry继承自WeakReference,被WeakReference关联的对象在下一次垃圾回收时会被回收。

WeakHashMap主要用来实现缓存,通过使用WeakHashMap来引用缓存对象,有JVM对这部分缓存进行回收。

private final ReferenceQueue<Object> queue = new ReferenceQueue<>();

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
    V value;
    final int hash;
    Entry<K,V> next;

    /**
     * Creates new entry.
     */
    Entry(Object key, V value,
          ReferenceQueue<Object> queue,
          int hash, Entry<K,V> next) {
        super(key, queue);
        this.value = value;
        this.hash  = hash;
        this.next  = next;
    }
}

参考

CS-Notes
HashMap详解 基于jdk1.7
最新JDK8HashMamp实现过程源码分析(二)
Java8源码-HashMap
Hashmap的结构,1.7和1.8有哪些区别
ConcurrentHashMap1.8源码源码分析(2)
谈谈ConcurrentHashMap1.7和1.8的不同实现
ConcurrentHashMap源码分析(1.8)
WeakHashMap详细介绍(源码解析)和使用示例
WeakReference 学习和使用

猜你喜欢

转载自blog.csdn.net/qq_21687635/article/details/89840257