JDK源码之各容器源码分析

一、ArrayList源码解析

首先我们来看下ArrayList中几个重要的属性:

// 序列化ID
    private static final long serialVersionUID = 8683452581122892189L;
    
    // 默认初始化容量
    private static final int DEFAULT_CAPACITY = 10;

    // 含参构造器中容量设置为0时的空数组
    private static final Object[] EMPTY_ELEMENTDATA = {};

    // 默认构造器中的空数组,在第一次添加元素时数组容量扩充为默认初始化容量
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    // 存储数据元素的数组
    transient Object[] elementData; // non-private to simplify nested class access

    // 当前arraylist集合的大小,也就是elementData数组中数据元素的个数
    private int size;

1.1 构造器

接着我们看下ArrayList的构造方法,ArrayList的构造方法支持三种形式:

 /**
     * 形式一:无参构造方法
     */
    public ArrayList() {
    	//第一次添加元素时数组才会扩充为默认初始化容量10
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

我们可以看到,在构造方法中直接将 elementData 指向 DEFAULTCAPACITY_EMPTY_ELEMENTDATA空数组,这个时候该ArrayList的size为初始值0。

  /**
     * 形式二:携带一个int类型的参数,指定arraylist的初始容量
     */
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

参数initialCapacity为我们所指定的arraylist初始容量,可以看出,方法中对initialCapacity的值进行了一系列判断,当我们所指定的初始容量小于0时无意义,直接抛出非法参数异常。当initialCapacity大于0时,会直接创建一个Object类型的数组,数组的初始大小就是initialCapacity的值。当initialCapacity等于0时,会直接将elementData 指向EMPTY_ELEMENTDATA空数组。

    /**
     * 形式三:携带一个Collection类型的参数
     */
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

代码中首先将Collection参数通过toArray方法转换成数组,并赋值给elementData,然后对arraylist中的size进行赋值并判断size是否等于0。当size为0时,直接将elementData 指向EMPTY_ELEMENTDATA空数组。当size不为0时执行copyOf方法。

1.2 add方法

ArrayList的构造方法到这里就结束了,接着我们分析下add方法。add方法根据参数个数的不同有两种,如下所示:

    /**
     * 方式一:直接添加数据元素到arraylist的尾部
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

    /**
     * 方式二:插入数据元素到特定的角标位置
     */
    public void add(int index, E element) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

我们首先对方式一进行分析下。方法中首先调用到ensureCapacityInternal方法,将size+1作为参数传入。我们知道,size用来表示当前arraylist的大小,也就是elementData数组中元素的个数,size+1就是确保数据元素添加成功的最小容量。ensureCapacityInternal方法是做什么的?让我们跟进去看下:

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

        ensureExplicitCapacity(minCapacity);
    }

可以看到方法中首先将elementData 和DEFAULTCAPACITY_EMPTY_ELEMENTDATA进行对比,如果elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,则执行 if 语句体的操作,否则直接调用ensureExplicitCapacity方法,将最小容量minCapacity作为参数传入。什么情况下elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA呢?不知道大家还有没有印象,当我们通过ArrayList的无参构造方法创建ArrayList对象时,在构造方法中会直接将elementData 指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA。也就是说当我们通过ArrayList的无参构造方法创建ArrayList对象后,再调用add方法的时候会执行if 语句体操作,将minCapacity 重新赋值为DEFAULT_CAPACITY和minCapacity中的最大值。DEFAULT_CAPACITY我们之前也有讲过,为默认初始化容量。当我们通过ArrayList的无参构造函数创建ArrayList对象后,首次调用add方法时,这个时候ensureCapacityInternal方法中传入的minCapacity为1,if 语句体中minCapacity重新赋值为10。我们接着看下ensureExplicitCapacity方法:

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

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

可以看到,方法中首先将变量modCount自增1,modCount是做什么用的呢?其实modCount是用来标记当前arraylist集合操作变化的次数,在fail-fast机制中会有用到这个变量,关于fail-fast机制我们稍后会讲下。接着判断minCapacity - elementData.length 是否大于0,当minCapacity - elementData.length大于0的时候说明当前elementData数组大小不够用,需要扩容,grow方法就是具体的扩容操作,我们跟进去看下:

    /**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     *
     * @param minCapacity the desired minimum capacity
     */
    private void grow(int minCapacity) {
        // 1.首先获取到elementData数组的长度,作为原容量
        int oldCapacity = elementData.length;
        // 2.新容量 = 原容量 + 原容量/2;   1.5倍扩容
        int newCapacity = oldCapacity + (oldCapacity >> 1);
    
        if (newCapacity - minCapacity < 0)
            // 3.若1.5倍扩容后还不够,则将最小容量作为新容量
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            // 4.限制最大容量
            newCapacity = hugeCapacity(minCapacity);
        // 5.进行原有数据元素copy处理
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

fail-fast机制
上述add方法中含有一个变量modCount,此变量使用方法类似于java中轻量级锁及数据库所用到的CAS操作。一般情况下有两种情况,一种情况发生在多线程操作,当a线程正在通过迭代器操作集合mDatas时,同时b线程对mDatas进行添加或者删除元素,会触发fail-fast机制,抛出该异常。另一种情况是在迭代集合mDatas的过程中对mDatas集合进行元素添加或者删除操作时,会触发fail-fast机制,抛出该异常。这两种情况都是在操作之前先记录当前modCount的数值,若在迭代器遍历集合的过程中没有其他线程或此循环内向集合中添加或删除元素,则modCount与此前一致,操作成功。反之,若有同步线程或循环体内向集合中增加或删除了元素,modCount++,与之前数值不同,抛出ConcurrentModificationException异常。

ArrayList的迭代器为其的一个内部类,如下所示,从中可以看到每次迭代器查找下一个元素时都会访问modCount,遵循fail-fast机制。

 private class Itr implements Iterator<E> {
       
        protected int limit = ArrayList.this.size;

        int cursor;       // next 元素角标
        int lastRet = -1; // last 元素角标
        int expectedModCount = modCount;         //重点:将modCount的值赋值给expectedModCount(期待修改数量)
        
        // 判断是否有下一个元素
        public boolean hasNext() {
            return cursor < limit;
        }

        @SuppressWarnings("unchecked")
        public E next() {                        // 获取到next元素
            // 1. 当modCount != expectedModCount时,会抛出ConcurrentModificationException异常
            if (modCount != expectedModCount)        
                throw new ConcurrentModificationException();
            int i = cursor;
            // 2.角标越界检测
            if (i >= limit)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            // 3.cursor的值自增1
            cursor = i + 1;
            // 4.对lastRet进行赋值并将当前角标对应的数据元素return掉
            return (E) elementData[lastRet = i];
        }

        public void remove() {
            // 1.角标越界检测
            if (lastRet < 0)
                throw new IllegalStateException();
            // 2. 当modCount != expectedModCount时,会抛出ConcurrentModificationException异常
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();

            try {
                // 3.调用arraylist的remove方法,进行remove操作
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                // 4.重点:在3 处调用arraylist的remove方法,进行remove操作时,会将modCount的值自增1,在这里重新对expectedModCount进行赋值操作,否则会导致modCount != expectedModCount,抛出ConcurrentModificationException异常。
                expectedModCount = modCount;
                limit--;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;

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

另一个重载的add方法
上面我们分析了add方法的第一种方式,直接添加数据元素到arraylist的尾部。简单讲下,首先会处理elementData数组是否需要扩容操作,接着对新添加的数据元素进行赋值操作,size自增1,最后return true表示数据添加成功。add方法的第二种方式和第一种方式类似,插入数据元素到特定的角标位置,方法中首先会对下角标index 进行越界判断,然后会对elementData数组是否需要扩容操作进行处理,接着调用 System.arraycopy方法将指定角标后的元素后移一位,最后对指定角标位置进行赋值操作并将size自增1。

1.3 remove方法

add方法到这里就结束了,下面我们接着看remove方法,remove方法根据参数的不同也同样分为两种,方法如下:

 /*
    * 方式一:根据角标进行remove操作
    */
    public E remove(int index) {
        // 1. 对角标越界进行判断
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        // 2.modCount自增1
        modCount++;
        // 3.获取到指定下角标位置的数据
        E oldValue = (E) elementData[index];
        // 4.计算需要移动的元素个数
        int numMoved = size - index - 1;
        if (numMoved > 0)
            // 5. 指定角标位置后的元素前移一位
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        // 6.将size自减1,并将数组末尾置为null,便于垃圾回收
        elementData[--size] = null; // clear to let GC do its work
        // 7.最后将所要删除的数据元素return掉
        return oldValue;
    }

    /*
     * 方式二:根据数据元素进行remove操作
     */
    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

对于方式一,根据角标进行remove操作,代码中的注释已经很清楚了,下面我们看下方式二,根据数据元素进行remove操作。方法中首先对当前remove的数据元素进行null判断。无论当前remove的数据元素是否为null,都需要一个for循环进行遍历操作,注意if 条件块的代码在数据元素为null和不为null两种情况下是不同的。如果elementData数组中某一角标处的元素等于当前remove的数据元素,会调用fastRemove方法进行具体remove操作,最后return true表示remove操作成功。否则return false表示remove操作失败。fastRemove方法的代码如下:

  /*
     * Private remove method that skips bounds checking and does not
     * return the value removed.
     */
    private void fastRemove(int index) {
        // 1.modCount的值自增1
        modCount++;
        // 2.计算需要移动的元素个数
        int numMoved = size - index - 1;
        if (numMoved > 0)
             // 3. 指定角标位置后的元素前移一位
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        // 4.将size自减1,并将数组末尾置为null,便于垃圾回收
        elementData[--size] = null; // clear to let GC do its work
    }

其他方法参考此文:https://www.jianshu.com/p/f863791e77fe

二、HashMap源码分析

这里仅对一些HashMap原理性代码进行分析,其他方法的具体分析参加此篇深度好文:https://segmentfault.com/a/1190000012926722

2.1 原理

HashMap 底层是基于散列算法实现,散列算法分为散列再探测和拉链式。HashMap 则使用了拉链式的散列算法,并在 JDK 1.8 中引入了红黑树优化过长的链表。数据结构示意图如下:
在这里插入图片描述
对于拉链式的散列算法,其数据结构是由数组和链表(或树形结构)组成。在进行增删查等操作时,首先要定位到元素的所在桶的位置,之后再从链表中定位该元素。比如我们要查询上图结构中是否包含元素35,步骤如下:

  1. 定位元素35所处桶的位置,index = 35 % 16 = 3
  2. 在3号桶所指向的链表中继续查找,发现35在链表中。

上面就是 HashMap 底层数据结构的原理,HashMap 基本操作就是对拉链式散列算法基本操作的一层包装。不同的地方在于 JDK 1.8 中引入了红黑树,底层数据结构由数组+链表变为了数组+链表+红黑树,不过本质并未变。好了,原理部分先讲到这,接下来说说源码实现。

2.2 构造方法

HashMap 的构造方法不多,只有四个。HashMap 构造方法做的事情比较简单,一般都是初始化一些重要变量,比如 loadFactor 和 threshold。而底层的数据结构则是延迟到插入键值对时再进行初始化。

2.2.1初始容量、负载因子、阈值

我们在一般情况下,都会使用无参构造方法创建 HashMap。但当我们对时间和空间复杂度有要求的时候,使用默认值有时可能达不到我们的要求,这个时候我们就需要手动调参。在 HashMap 构造方法中,可供我们调整的参数有两个,一个是初始容量 initialCapacity,另一个负载因子 loadFactor。通过这两个设定这两个参数,可以进一步影响阈值大小。但初始阈值 threshold 仅由 initialCapacity 经过移位操作计算得出。他们的作用分别如下:

名称用途
initialCapacityHashMap 初始容量
loadFactor负载因子
threshold当前 HashMap 所能容纳键值对数量的最大值,超过这个值,则需扩容

相关代码如下:

/** The default initial capacity - MUST be a power of two. */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

/** The load factor used when none specified in constructor. */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

final float loadFactor;

/** The next size value at which to resize (capacity * load factor). */
int threshold;

如果大家去看源码,会发现 HashMap 中没有定义 initialCapacity 这个变量,仅有DEFAULT_INITIAL_CAPACITY 变量(显然是此变量在无参构造器中使用,而在HashMap中没有initialCapacity,那么对于含参后构造器如何初始化其容量呢?在介绍HashMap构造器时会说明)。这个也并不难理解,从参数名上可看出,这个变量表示一个初始容量,只是构造方法中用一次,没必要定义一个变量保存。

默认情况下,HashMap 初始容量是16,负载因子为 0.75。这里并没有默认阈值,原因是阈值可由容量乘上负载因子计算而来(注释中有说明),即threshold = capacity * loadFactor。但当你仔细看构造方法3时(看下文),会发现阈值并不是由上面公式计算而来,而是通过一个特殊方法算出来的,且计算结果并不是capacity 。接下来,我们来看看初始化 threshold 的方法长什么样的的,源码如下:

/**
 * Returns a power of two size for the given target capacity.
 */
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;
}

上面的代码长的有点不太好看,反正我第一次看的时候不明白它想干啥。不过后来在纸上画画,知道了它的用途。总结起来就一句话:找到大于或等于 cap 的最小2的幂。至于为啥要这样,后面再解释。我们先来看看 tableSizeFor 方法的图解:
在这里插入图片描述
上面是 tableSizeFor 方法的计算过程图,这里cap = 536,870,913 = 229 + 1,多次计算后,算出n + 1 = 1,073,741,824 = 230。通过图解应该可以比较容易理解这个方法的用途,即找到大于等于cap的2的最小整数次幂。

说完了初始阈值的计算过程,再来说说负载因子(loadFactor)。对于 HashMap 来说,负载因子是一个很重要的参数,该参数反应了 HashMap 桶数组的使用情况(假设键值对节点均匀分布在桶数组中)。通过调节负载因子,可使 HashMap 时间和空间复杂度上有不同的表现。当我们调低负载因子时,HashMap 所能容纳的键值对数量变少。扩容时,重新将键值对存储新的桶数组里,键的键之间产生的碰撞会下降,链表长度变短。此时,HashMap 的增删改查等操作的效率将会变高,这里是典型的拿空间换时间。相反,如果增加负载因子(负载因子可以大于1),HashMap 所能容纳的键值对数量变多,空间利用率高,但碰撞率也高。这意味着链表长度变长,效率也随之降低,这种情况是拿时间换空间。至于负载因子怎么调节,这个看使用场景了。一般情况下,我们用默认值就可以了。

2.2.2 构造器源码

HashMap 的构造方法不多,只有四个。HashMap 构造方法做的事情比较简单,一般都是初始化一些重要变量,比如 loadFactor 和 threshold。而底层的数据结构则是延迟到插入键值对时再进行初始化。HashMap 相关构造方法如下:

/** 构造方法 1 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

/** 构造方法 2 */
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/** 构造方法 3 */
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    
    //这里被赋值的threshold充当initialCapacity的作用,即记录HashMap的初始容量,在添加第一个元素时,
    //创建的数组初始大小即为threshold中记录的值。添加完毕后,threshold恢复其原始作用,大小变为capacity * loadFactor。
    this.threshold = tableSizeFor(initialCapacity);		
}

/** 构造方法 4 */
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

上面4个构造方法中,大家平时用的最多的应该是第一个了。第一个构造方法很简单,仅将 loadFactor 变量设为默认值。构造方法2调用了构造方法3,而构造方法3仍然只是设置了一些变量。构造方法4则是将另一个 Map 中的映射拷贝一份到自己的存储结构中来,这个方法不是很常用。

上面就是对构造方法简单的介绍,构造方法本身并没什么太多东西,所以就不说了。

2.3 查找

HashMap 的查找操作比较简单,查找步骤与原理篇介绍一致,即先定位键值对所在的桶的位置,然后再对链表或红黑树进行查找。通过这两步即可完成查找,该操作相关代码如下:

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 1. 定位键值对所在桶的位置
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {		//tab[(n-1)&hash]:取余操作(优化)
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            // 2. 如果 first 是 TreeNode 类型,则调用黑红树查找方法
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                
            // 2. 对链表进行查找
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

2.2.1 Hash算法中取余的优化

查找的核心逻辑是封装在 getNode 方法中的,getNode 方法源码我已经写了一些注释,应该不难看懂。我们先来看看查找过程的第一步 - 确定桶位置,其实现代码如下:

// index = (n - 1) & hash
first = tab[(n - 1) & hash]

这里通过(n - 1)& hash即可算出桶的在桶数组中的位置。HashMap 中桶数组的大小 length 总是2的幂,此时,(n - 1) & hash 等价于对 length 取余。但取余的计算效率没有位运算高,所以(n - 1) & hash也是一个小的优化。举个例子说明一下吧,假设 hash = 185,n = 16。计算过程示意图如下:
在这里插入图片描述
2.2.2 对象的hash码计算

在上面源码中,除了查找相关逻辑,还有一个计算 hash 的方法。这个方法源码如下:

/**
 * 计算键的 hash 值
 */
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

看这个方法的逻辑好像是通过位运算重新计算 hash,那么这里为什么要这样做呢?为什么不直接用键的 hashCode 方法产生的 hash 呢?这样做有两个好处,简单解释一下。我们再看一下上面求余的计算图,图中的 hash 是由键的 hashCode 产生。计算余数时,由于 n 比较小,hash 只有低4位参与了计算,高位的计算可以认为是无效的。这样导致了计算结果只与低位信息有关,高位数据没发挥作用。为了处理这个缺陷,我们可以上图中的 hash 高4位数据与低4位数据进行异或运算,即 hash ^ (hash >>> 4)。通过这种方式,让高位数据与低位数据进行异或,以此加大低位信息的随机性,变相的让高位数据参与到计算中。此时的计算过程如下:
在这里插入图片描述
在 Java 中,hashCode 方法产生的 hash 是 int 类型,32 位宽。前16位为高位,后16位为低位,所以要右移16位。

上面所说的是重新计算 hash 的一个好处,除此之外,重新计算 hash 的另一个好处是可以增加 hash 的复杂度。当我们覆写 hashCode 方法时,可能会写出分布性不佳的 hashCode 方法,进而导致 hash 的冲突率比较高。通过移位和异或运算,可以让 hash 变得更复杂,进而影响 hash 的分布性。这也就是为什么 HashMap 不直接使用键对象原始 hash 的原因了。

2.4、遍历

大家在遍历 HashMap 的过程中会发现,多次对 HashMap 进行遍历时,遍历结果顺序都是一致的。但这个顺序和插入的顺序一般都是不一致的。产生上述行为的原因是怎样的呢?代码如下:

public Set<K> keySet() {
    Set<K> ks = keySet;
    if (ks == null) {
        ks = new KeySet();
        keySet = ks;
    }
    return ks;
}

/**
 * 键集合
 */
final class KeySet extends AbstractSet<K> {
    public final int size()                 { return size; }
    public final void clear()               { HashMap.this.clear(); }
    public final Iterator<K> iterator()     { return new KeyIterator(); }
    public final boolean contains(Object o) { return containsKey(o); }
    public final boolean remove(Object key) {
        return removeNode(hash(key), key, null, false, true) != null;
    }
    // 省略部分代码
}

/**
 * 键迭代器
 */
final class KeyIterator extends HashIterator 
    implements Iterator<K> {
    public final K next() { return nextNode().key; }
}

abstract class HashIterator {
    Node<K,V> next;        // next entry to return
    Node<K,V> current;     // current entry
    int expectedModCount;  // for fast-fail
    int index;             // current slot

    HashIterator() {
        expectedModCount = modCount;
        Node<K,V>[] t = table;
        current = next = null;
        index = 0;
        if (t != null && size > 0) { // advance to first entry 
            // 寻找第一个包含链表节点引用的桶
            do {} while (index < t.length && (next = t[index++]) == null);
        }
    }

    public final boolean hasNext() {
        return next != null;
    }

    final Node<K,V> nextNode() {
        Node<K,V>[] t;
        Node<K,V> e = next;
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
        if ((next = (current = e).next) == null && (t = table) != null) {
            // 寻找下一个包含链表节点引用的桶
            do {} while (index < t.length && (next = t[index++]) == null);
        }
        return e;
    }
    //省略部分代码
}

如上面的源码,遍历所有的键时,首先要获取键集合KeySet对象,然后再通过 KeySet 的迭代器KeyIterator进行遍历。KeyIterator 类继承自HashIterator类,核心逻辑也封装在 HashIterator 类中。HashIterator 的逻辑并不复杂,在初始化时,HashIterator 先从桶数组中找到包含链表节点引用的桶。然后对这个桶指向的链表进行遍历。遍历完成后,再继续寻找下一个包含链表节点引用的桶,找到继续遍历。找不到,则结束遍历。举个例子,假设我们遍历下图的结构:
在这里插入图片描述
HashIterator 在初始化时,会先遍历桶数组,找到包含链表节点引用的桶,对应图中就是3号桶。随后由 nextNode 方法遍历该桶所指向的链表。遍历完3号桶后,nextNode 方法继续寻找下一个不为空的桶,对应图中的7号桶。之后流程和上面类似,直至遍历完最后一个桶。以上就是 HashIterator 的核心逻辑的流程,对应下图:
在这里插入图片描述
遍历上图的最终结果是 19 -> 3 -> 35 -> 7 -> 11 -> 43 -> 59

结语:以上只是关于HashMap极少的一部分源代码,更为重点的插入操作以及删除操作涉及到红黑树,其插入时的链表树化、扩容机制、红黑树的拆分、链表的拆分等都十分经典,但这些内容篇幅过长,还是参考这篇深度好文(3.4 插入,3.5 删除)进行学习:https://segmentfault.com/a/1190000012926722

参考:

https://segmentfault.com/a/1190000012926722

三、HashMap与HashTable的区别

3.1 HashTable

Hashtable已经被弃用的一个类,性能比较低,它有一些自己的特点,不知道你发现没有,它不符合大小驼

峰命名规则,这点很讨厌。

  1. Hashtable的方法几乎都是同步的,都有synchronized关键字修饰,因此和HashMap相比,它是线程安全的。
    public synchronized V put(K key, V value) {...}

  2. Hashtable中key-value的映射,key和value
    都是不允许为null的,如果为null了呢?对不起,空指针异常抛出。

  3. Hashtable在计算节点元素在哈希表中的位置使用的算法稍有区别,它有它的好处,但和HashMap的算法比起来明显性能低一些。

  4. Hashtable的扩容是原来容量的二倍加1(2n+1),源码参考:int newCapacity = (oldCapacity << 1) + 1;

3.2 HashTable与HashMap的主要区别

  1. HashMap是继承自AbstractMap类,而HashTable是继承自Dictionary类。不过它们都实现了同时实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口。
  2. Hashtable比HashMap多提供了elments() 和contains() 两个方法。
  3. HashMap的key-value支持key-value,null-null,key-null,null-value四种。而Hashtable只支持key-value一种(即 key和value都不为null这种形式)。既然HashMap支持带有null的形式,那么在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断,因为使用get的时候,当返回null时,你无法判断到底是不存在这个key,还是这个key就是null,还是key存在但value是null。
  4. 线程安全性不同。HashMap的方法都没有使用synchronized关键字修饰,都是非线程安全的,而Hashtable的方法几乎都是被synchronized关键字修饰的。但是,当我们需要HashMap是线程安全的时,怎么办呢?我们可以通过Collections.synchronizedMap(hashMap)来进行处理,亦或者我们使用线程安全的ConcurrentHashMap。ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。
  5. 初始容量大小和每次扩充容量大小的不同
    Hashtable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。
  6. 计算hash值的方法不同。为了得到元素的位置,首先需要根据元素的KEY计算出一个hash值,然后再用这个hash值来计算得到最终的位置。

Hashtable直接使用对象的hashCode。hashCode是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值。然后再使用除留余数发来获得最终的位置。
在这里插入图片描述
Hashtable在计算元素的位置时需要进行一次除法运算,而除法运算是比较耗时的。
HashMap为了提高计算效率,将哈希表的大小固定为了2的幂,这样在取模预算时,不需要做除法,只需要做位运算。位运算比除法的效率要高很多。

HashMap的效率虽然提高了,但是hash冲突却也增加了。因为它得出的hash值的低位相同的概率比较高。为了解决这个问题,HashMap重新根据hashcode计算hash值后,又对hash值做了一些运算来打散数据。使得取得的位置更加分散,从而减少了hash冲突。当然了,为了高效,HashMap只做了一些简单的位处理。从而不至于把使用2 的幂次方带来的效率提升给抵消掉。
在这里插入图片描述
**参考:**https://blog.csdn.net/luojishan1/article/details/81952147

四、ConcurrentHashMap

HashTable是一个线程安全的类,它使用synchronized来锁住整张Hash表来实现线程安全,即每次锁住整张表让线程独占,相当于所有线程进行读写时都去竞争一把锁,导致效率非常低下。ConcurrentHashMap可以做到读取数据不加锁,并且其内部的结构可以让其在进行写操作的时候能够将锁的粒度保持地尽量地小,允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的Hashtable,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。

4.1CouncurrentHashMap实现原理

ConcurrentHashMap 为了提高本身的并发能力,在内部采用了一个叫做 Segment 的结构,一个 Segment 其实就是一个类 Hash Table 的结构,Segment 内部维护了一个链表数组,我们用下面这一幅图来看下 ConcurrentHashMap 的内部结构,从下面的结构我们可以了解到,ConcurrentHashMap 定位一个元素的过程需要进行两次Hash操作,第一次 Hash 定位到 Segment,第二次 Hash 定位到元素所在的链表的头部,因此,这一种结构的带来的副作用是 Hash 的过程要比普通的 HashMap 要长,但是带来的好处是写操作的时候可以只对元素所在的 Segment 进行操作即可,不会影响到其他的 Segment,这样,在最理想的情况下,ConcurrentHashMap 可以最高同时支持 Segment 数量大小的写操作(刚好这些写操作都非常平均地分布在所有的 Segment上),所以,通过这一种结构,ConcurrentHashMap 的并发能力可以大大的提高。我们用下面这一幅图来看下ConcurrentHashMap的内部结构详情图,如下:
在这里插入图片描述
不难看出,ConcurrentHashMap采用了二次hash的方式,第一次hash将key映射到对应的segment,而第二次hash则是映射到segment的不同桶(bucket)中。

为什么要用二次hash,主要原因是为了构造分离锁,使得对于map的修改不会锁住整个容器,提高并发能力。当然,没有一种东西是绝对完美的,二次hash带来的问题是整个hash的过程比hashmap单次hash要长,所以,如果不是并发情形,不要使用concurrentHashmap。

原理上来说:ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。

JAVA7之前ConcurrentHashMap主要采用锁机制,在对某个Segment进行操作时,将该Segment锁定,不允许对其进行非查询操作,而在JAVA8之后采用CAS无锁算法,这种乐观操作在完成前进行判断,如果符合预期结果才给予执行,对并发操作提供良好的优化.。

让我们先看JDK1.7的ConcurrentHashMap的原理分析:

4.2 JDK1.7的ConcurrentHashMap

如上所示,是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表。

在JDK1.7中,ConcurrentHashMap的并发级别(允许的最大同时并发数)即ConcurrentHashMap中Segment类型数组的大小一旦初始化完成就不能改变(默认为16),初始化时Segmen类型t数组大小为大于等于指定参数的最小2的整数幂次。那么当ConcurrentHashMap内的数据越来越多时采用的是何种扩容机制呢?其实在扩容时,是将Segment中HashEntry类型数组的长度变为原来的二倍。

concurrencyLevel 一经指定,不可改变,后续如果ConcurrentHashMap的元素数量增加导致ConrruentHashMap需要扩容,ConcurrentHashMap不会增加Segment的数量,而只会增加Segment中链表数组的容量大小,这样的好处是扩容过程不需要对整个ConcurrentHashMap做rehash,而只需要对Segment里面的元素做一次rehash就可以了。

让我们看看Segment里面的成员变量,源码如下:

static final class Segment<K,V> extends ReentrantLock implements Serializable {
    transient volatile int count;    //Segment中元素的数量
    transient int modCount;          //对table的大小造成影响的操作的数量(比如put或者remove操作)
    transient int threshold;        //阈值,Segment里面元素的数量超过这个值那么就会对Segment进行扩容
    final float loadFactor;         //负载因子,用于确定threshold
    transient volatile HashEntry<K,V>[] table;    //链表数组,数组中的每一个元素代表了一个链表的头部
}

接着让我们继续看看JDK1.7中ConcurrentHashMap的成员变量:

// 默认初始容量(ConcurrentHashMap整体元素的初始容量)
static final int DEFAULT_INITIAL_CAPACITY = 16;
// 默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 默认segment层级,(最大并发量、ConcurrentHashMap中的Segment类型数组大小)
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 最大容量(ConcurrentHashMap的最大容量)
static final int MAXIMUM_CAPACITY = 1 << 30;
// segment最小容量
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
// 一个segment最大容量
static final int MAX_SEGMENTS = 1 << 16;
// 锁之前重试次数
static final int RETRIES_BEFORE_LOCK = 2;

一些常用方法的源码分析以及JDK1.8中ConcurrentHashMap的源码分析参照此篇深度好文https://www.cnblogs.com/huangjuncong/p/9478505.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值