java集合详解(思维导图)

451 阅读25分钟

话不多说,直接上操作!!

image.png

ArrayList源码分析

image.png

  • Iterable接口:提供迭代器访问能力,实现此接口允许对象通过foreach遍历。
  • Collection接口:集合的根接口
  • AbstractCollection抽象类:此类提供了Collection接口的基本实现,以最大程度减少实现此接口所需工作。
  • List接口:Collection接口的子接口
  • AbstractList抽象类:此类提供List接口的基本实现以最大程度减少由“随机访问”数据存储实现此接口所需要的工作。
  • RandomAccess接口:该标记接口提供支持随机访问的能力。
  • Cloneable接口:该标记接口提供实例克隆的能力。
  • Serializable接口:该标记接口提供类序列化或反序列化的能力。

属性

     //默认容量为10
    private static final int DEFAULT_CAPACITY = 10;

   //空数组,传入容量为0时使用,通过new ArrayList(0)创建
    private static final Object[] EMPTY_ELEMENTDATA = {};

    //空数组,与EMPTY_ELEMENTDATA区分开,通过new ArrayList()创建
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    //存储元素的数组,transient修饰表示不序列化该属性,因为ArrayList具有动态扩容的特性,数组中的元素会有剩余,通过writeObject和readObject实现序列化和反序列化
    transient Object[] elementData; // non-private to simplify nested class access

    //数组的实际长度
    private int size;

构造方法

public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
          //初始容量大于0,则按照指定容量构造
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
          //初始容量为0,初始化为空数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
          //初始容量小于0则异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

    /**
     * 无参构造器,懒初始化,在添加第一个元素时elementData扩容为DEFAULT_CAPACITY,减少内存开销
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    public ArrayList(Collection<? extends E> c) {
      //将集合转为Object[]类型数组
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            if (elementData.getClass() != Object[].class)
              // 利用Arrays的copyOf函数复制成Object[]类型的elementData
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            //如果传入集合长度为0,则初始化为空数组
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

add方法

1、add(E e)方法

添加指定元素到集合末尾

public boolean add(E e) {
  		//检查插入一个元素是否需要扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
  	//将元素插入到最后一位
        elementData[size++] = e;
        return true;
    }

private void ensureCapacityInternal(int minCapacity) {
  //确保传入的最小容量minCapacity
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

private static int calculateCapacity(Object[] elementData, int minCapacity) {
  //计算所需最小容量,如果当前list是空数组,最小容量就是默认容量和传入的minCapacity的最大值,否则最小容量就是minCapacity
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

private void ensureExplicitCapacity(int minCapacity) {
  			//修改次数+1,用于fail-fast机制
        modCount++;

        // 当所需的最小容量大于现有数组的长度时,需要进行扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
  			//新容量=旧容量+旧容量*0.5,也就是扩容为之前的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
  			//如果新容量小于所需的旧容量(还不够),那就按照旧容量的扩容
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
  			//如果新容量超过了所能分配的最大容量,则新容量的大小根据所需的最小容量重新计算
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 扩容完毕,将新容量复制给elementData
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

private static int hugeCapacity(int minCapacity) {
  			//所需容量大于MAX_ARRAY_SIZE时返回 Integer.MAX_VALUE,否则返回MAX_ARRAY_SIZE,其中MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
        if (minCapacity < 0) 
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

2、add(int index, E element)方法

将指定的元素插入到集合指定位置,将当前在该位置的元素及之后的元素向后移动。

public void add(int index, E element) {
  		//越界检查
        rangeCheckForAdd(index);
			//是否需要扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
  		//将elementData中index位置开始的元素复制到index+1开始的位置,复制的长度为size-index
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
  		//将该元素添加到指定下标位置
        elementData[index] = element;
  		//数组长度+1
        size++;
    }

3、addAll(Collection c)方法

将指定集合中所有元素追加到集合末尾

public boolean addAll(Collection<? extends E> c) {
				//将集合c转成object[]数组
        Object[] a = c.toArray();
  			//插入的集合长度
        int numNew = a.length;
  			//检查新容量size + numNew时是否需要扩容
        ensureCapacityInternal(size + numNew);  // Increments modCount
  			//将集合a中的所有元素拷贝到elementData的最后
        System.arraycopy(a, 0, elementData, size, numNew);
  			//数组长度扩大为size+numNew
        size += numNew;
  			//如果插入的集合不为空就返回true,否则返回false
        return numNew != 0;
    }

4、addAll(int index, Collection c)方法

从指定位置开始,将指定集合的所有元素插入到此集合中,将当前位于该位置的元素和所有的元素后移

public boolean addAll(int index, Collection<? extends E> c) {
  			//越界检查
        rangeCheckForAdd(index);
				//将要插入的集合c转成Object[]数组
        Object[] a = c.toArray();
  			//要插入的数组长度
        int numNew = a.length;
  			//插入之后的新长度size + numNew是否需要扩容	
        ensureCapacityInternal(size + numNew);  // Increments modCount

  			//指定位置后数组要移动的长度,如果index恰好等于size就不需要移动
        int numMoved = size - index;
        if (numMoved > 0)
          //将elementData中index开始的元素复制到elementData中index+numNew的位置,复制长度为numMoved
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);
				//将添加的数组a复制到elementData中index开始的位置,复制长度为numNew
        System.arraycopy(a, 0, elementData, index, numNew);
  			//增加后的数组长度为size+numNew
        size += numNew;
        return numNew != 0;
    }

remove方法

1、remove(int index)

删除集合中指定位置的元素,将所有后续元素向左移动

public E remove(int index) {
  	//越界检查
    rangeCheck(index);
		//修改次数+1,用于fail-fast机制
    modCount++;
  	//取出要删除的元素
    E oldValue = elementData(index);

  	//删除该下标的元素,后面元素要移动的元素,最好的情况是index=size-1即最后一个元素,那就不需要移动元素
    int numMoved = size - index - 1;
    if (numMoved > 0)
      	//将elementData中index+1位置开始的元素复制到index开始的位置,复制长度为numMoved
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
  	//将最后一个元素置为null,方便GC回收,删除的时候并没有缩小容量
    elementData[--size] = null; // clear to let GC do its work
  	//返回删除的元素
    return oldValue;
}

2、remove(Object o)

从集合中删除第一次出现的指定元素,如果存在,则将其删除。如果集合中不包含该元素,则保持不变

public boolean remove(Object o) {
    if (o == null) {
      	//如果删除的元素为null,遍历elementData数组,用==比较,找到第一个值为null的,快速删除对应的下标
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
      //遍历所有的elementData,用equal比较两个对象,找到第一次相等位置的下标快速删除
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
  	//没有找到对应的元素返回false
    return false;
}

private void fastRemove(int index) {
  			//修改次数+1,用于fail-fast机制
        modCount++;
  			//需要移动的长度,如果index恰好为size-1,即删除最后一个元素时不需要移动
        int numMoved = size - index - 1;
        if (numMoved > 0)
          	//将elementData中index+1开始的元素复制到index开始的位置,复制长度为numMoved
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
  			//将最后一个元素置为null,方便GC回收,删除的时候并没有缩小容量
        elementData[--size] = null; // clear to let GC do its work
    }

3、removeAll(Colleciton c)

从集合中删除指定的集合中包含的所有元素

public boolean removeAll(Collection<?> c) {
  //非空检查
    Objects.requireNonNull(c);
  //批量删除包含集合c的元素
    return batchRemove(c, false);
}

//complement为true表示删除不包含在c中的元素,求两个集合的交集,complement为false表示删除包含在集合c中的元素,求两个集合的差集
private boolean batchRemove(Collection<?> c, boolean complement) {
        final Object[] elementData = this.elementData;
  		//r为写指针,w为读指针
        int r = 0, w = 0;
  		//是否修改的标志
        boolean modified = false;
        try {
            for (; r < size; r++)
              //当c中不包含elementData[r]complement=false时, elementData[w++]存放的保留元素就是elementData中除去集合c有的元素
              //当c中包含elementData[r]complement=true时,elementData[w++]存放的保留元素就是elementData和集合c的交集
                if (c.contains(elementData[r]) == complement)
                    elementData[w++] = elementData[r];
        } finally {
            // 正常循环结束,r==size,否则c.contain()抛出了异常
            if (r != size) {
              //将elementData中r位置开始的元素复制到elementData中w开始的位置,复制长度为size-r,也就是将出错位置r开始的所有元素移动到已经保留的元素w位置之后
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
              	//更新保留的元素个数
                w += size - r;
            }
          	//如果w==size,表示全部元素保留,没有修改,返回false
            if (w != size) {
                // 将未保留的元素置为null,方便GC回收
                for (int i = w; i < size; i++)
                    elementData[i] = null;
              	// 修改的次数+未保留的元素个数
                modCount += size - w;
                size = w;
              	//标记修改成功
                modified = true;
            }
        }
  			//修改失败返回false
        return modified;
    }

HashSet源码分析

类定义

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
  • 是一个泛型类
  • 继承自AbstractSet,实现了Set接口
  • 实现了Cloneable接口,表示HashSet支持克隆
  • 实现了Serializable,表示支持序列化和反序列化

字段属性

//使用HashMap存放数据,从泛型E可以看出HashSet的值就是hashMap中的key
private transient HashMap<E,Object> map;

// 默认对象,存入map中的value
private static final Object PRESENT = new Object();

从字段属性可以看出:hashSet底层使用HashMap存储数据,hashSet的数据实际就是hashMap的key,value默认为创建的PRESENT对象。

构造方法

    //默认的构造方法,初始化map
		public HashSet() {
        map = new HashMap<>();
    }

    //传入一个集合对象的构造方法
    public HashSet(Collection<? extends E> c) {
      //初始化hashMap,(c.size()/.75f) + 1作为默认长度,如果小于HashMap的默认长度16,则使用HashMap的默认长度
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }

    //传入初始化容量和负载因子
    public HashSet(int initialCapacity, float loadFactor) {
      //调用hashMap的构造方法
        map = new HashMap<>(initialCapacity, loadFactor);
    }

    //传入初始化容量
    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }

    //这个构造方法的意义是让LinkedHashSet使用,因为初始化的map是LinkedHashMap,dummy没什么用
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }

其他方法

1、add方法

public boolean add(E e) {
  //调用map添加,从这可以看出hashSet的元素实际上就是hashMap的key
  return map.put(e, PRESENT)==null;
}

2、remove方法

public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
}

可以看出,都是调用map方法进行的操作。

TreeSet的应用

1、首先定义一个Student类,需要实现hashCode和equals方法

public class Student {

    private String name;

    private int age;

    public Student() {}

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public int hashCode() {
        return age*31+name.hashCode()*31;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Student) {
          Student s = (Student) obj;
          if (this.age == s.getAge() && this.name.equals(s.getName())) {
              return true;
          }
        }
        return false;
    }
}

2、测试类

public class TreeSetTest {

    public static void main(String [] args) {
        Set<Student> set = new TreeSet<>(new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                //按照年龄倒叙排序
                return o2.getAge()-o1.getAge();
            }
        });
        Student s1 = new Student("小明", 12);
        Student s2 = new Student("小华", 13);
        Student s3 = new Student("小坤", 14);
        set.add(s1);
        set.add(s2);
        set.add(s3);
        for (Student student : set) {
            System.out.println(student.getName()+"-"+student.getAge());
        }
    }
}

输出结果: /* 输出结果 小坤-14 小华-13 小明-12 */

Comparable和Comparator

Comparable

Comparable接口定义很简单,源码如下:

public interface Comparable<T> {   
  int compareTo(T t);
}

如果一个类实现了Comparable接口,只需要重写compareTo方法,就可以按照自己制定的规则将由它创建的对象进行比较。

举例

1、定义一个Stundet类,实现Comparable接口,重写compareTo方法

public class Student implements Comparable<Student>{

    private String name;

    private int age;

    public Student() {}

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public int hashCode() {
        return age*31+name.hashCode()*31;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Student) {
          Student s = (Student) obj;
          if (this.age == s.getAge() && this.name.equals(s.getName())) {
              return true;
          }
        }
        return false;
    }

    @Override
    public int compareTo(Student o) {
        //按照年龄顺序排序
        return this.age-o.getAge();
    }
}

2、测试类

public class ComparableTest {

    public static void main(String [] args) {
        List<Student> list = new ArrayList<>();
        Student s1 = new Student("小敏", 12);
        Student s2 = new Student("小建", 11);
        Student s3 = new Student("小可", 10);
        list.add(s1);
        list.add(s2);
        list.add(s3);
        Collections.sort(list);
        for (Student student : list) {
            System.out.println("name="+student.getName()+",age="+student.getAge());
        }
    }
}

3、输出结果: name=小可,age=10 name=小建,age=11 name=小敏,age=12

CompareTo方法返回值与排序效果

  • 返回负数:代表当前对象小于传入的对象。
  • 返回0:代表当前对象等于传入的对象。
  • 返回正数:代表当前对象大于传入的对象。

Comparator

相较于Comparable复杂,核心方法只有两个:

public interface Comparator<T> { 
  //返回值可能是负数、零或者正数,代表第一个对象小于、等于或大于第二个对象
  int compare(T o1, T o2);  
  //需要传入一个Object作为参数,并判断该Object是否和Comparator保持一致
  boolean equals(Object obj);
}

自定义比较器

public class ComparatorStudent implements Comparator<Student>{

    @Override
    public int compare(Student o1, Student o2) {
        //先按照年龄顺序排序,年龄一样,再按照名字的字母表顺序排序
        if (o1.getAge() < o2.getAge()) {
            return -1;
        } else if (o1.getAge() == o2.getAge()) {
            if (o1.getName().compareTo(o2.getName()) <0) {
                return -1;
            } else {
                return 1;
            }
        } else {
            return 1;
        }
    }
}

测试类:

public class ComparatorTest {

    public static void main(String [] args) {
        List<Student> list = new ArrayList<>();
        Student s1 = new Student("xiaoming", 12);
        Student s2 = new Student("haha", 10);
        Student s3 = new Student("nihao", 10);
        list.add(s1);
        list.add(s2);
        list.add(s3);
        list.sort(new ComparatorStudent());
        for (Student student : list) {
            System.out.println("name="+student.getName()+",age="+student.getAge());
        }
    }
}
/*
输出结果:(先按年龄顺序排序,再按照名字的字母表顺序排序)
name=haha,age=10
name=nihao,age=10
name=xiaoming,age=12
*/

总结

  • Comparable位于java.lang包下,属于内部比较器,一个类如果想要使用Collections.sort(list)进行排序,则需要实现该接口
  • Comparator位于java.util包下,属于外部比较器,对于那些没有实现Comparable接口或者对Comparable排序规则不满意的可以自定义类比较器。

Map

HashMap的java7实现

image.png

HashMap里面是一个数组,数组中每个元素是一个单向链表。上图中每个绿色的实体是嵌套类Entry的实例,Entry包含四个属性:key,value,hash值和指向下一元素的next指针。

HashMap的java8实现

image.png Java8对HashMap做了一些修改,最大不同是利用了红黑树,结构变成了数组+链表+红黑树。根据Java7的数组+链表实现,查找时根据hash值可以快速定位到数组的具体下标。之后,就要顺着链表查找那个唯一的元素,时间复杂度取决于链表的长度O(n)。为了降低这部分开销,当链表长度超过一定值(默认为8),会将链表转换为红黑树,在链表上查找的时间复杂度变为O(logN)

初始容量为什么是16

容量就是一个hashMap中“桶”的个数,当我们要往一个HashMap中国put一个元素时,需要通过一定的算法(hash算法)计算出应该把它放到哪个桶里,这个过程就叫做哈希。对应的就是hash()方法。哈希的功能就是根据key来定位这个K-V在链表数组中的位置,也就是hash方法的输入应该是个Object类型的key,输出是个int类型的数组下标。

image.png

hash的实现是由两个方法 int hash(Object k)和int indexFor(int h, int length)实现的。我们来看下indexFor()方法,java8虽然没有这样一个单独的方法,但是查询下标的算法和java7是一样的:

static int indexFor(int h, int length) {
    return h & (length-1);
}

indexFor主要是将hashCode转换成链表数组中的下标,h表示元素的hashCode值,length表示hashMap的容量。h&(length-1)就是取模运算,之所以用位运算是因为位运算更快,原理是:X % 2^n = X & (2^n - 1) 既然length需要是2的幂,那么为什么一定是16呢?可以是4,8,32吗?其实16是一个经验值,在效率和空间上做的一个权衡,这个值不能太大,太大了浪费空间,也不能太小,太小了可能频繁触发扩容。

负载因子

什么是负载因子

第一次创建HashMap时,就会指定容量(不指定默认为16),随着我们不断put元素,就有可能超过容量,那么就需要扩容。

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);  
} 

从代码可以看出,在添加元素时,如果元素数量超过临界值threshold时,就会自动扩容,并且扩容之后,还需要对HashMap中原有元素进行rehash,将原来桶中元素重新分配到新的桶中,在hashMap中,临界值threshold = 负载因子(loadFactor)* 容量(capacity)。负载因子表示HashMap满的程度,默认值为0.75,也就是说默认情况下,HashMap中元素个数达到了容量的3/4就会自动扩容。

为什么要扩容

HashMap再扩容不仅要对容量扩充,还要rehash,这个过程很耗时,且随着元素越多越好时。为什么还要扩容?这跟哈希碰撞有关。hashMap底层是基于哈希函数实现的,哈希函数有个特点:根据同一哈希函数计算出的散列值如果不同,输入值肯定不同,但是计算出的散列值相同的输入值也不一定相同。两个不同的输入值,根据同一哈希函数计算出的散列值相同的现象就叫做碰撞。解决碰撞的方法很多,其中链地址法比较常见,也是hashMap采用的。

image.png

  • 当put元素时,先定位到是数组中的哪个链表,然后把元素挂到这个链表的后面。
  • 当get元素时,先定位到是数组的哪个链表,然后逐一遍历链表的元素,直到找到需要的元素为止。 解决碰撞最有效的方法是扩大数组容量,再通过一个合适的hash算法计算元素分配到哪个桶里,就可以大大减少冲突的概率。

为什么默认是0.75

JDK官方文档描述:默认的负载因子0.75在时间和空间成本之间提供了很好的权衡,更高的减少了空间开销,但增加了查找成本。 如果负载因子为1,就表示hashMap满了才扩容,那么在hashMap中最好的情况就是16个元素平均分配到16个桶里,否则必然会碰撞。随着元素越多碰撞概率越大。

为什么是线程不安全的

HashMap的线程不安全体现在会造成死循环、数据丢失以及数据覆盖这些问题。其中死循环和数据丢失是在JDK1.7出现的问题,在JDK1.8已经得到解决。但是1.8还是有数据覆盖的问题。

扩容引发的数据不安全

HashMap的线程不安全主要发生在扩容函数里,根源在transfer函数中,JDK1.7中HashMap的transfer函数如下:

void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
              	//重新计算每个桶的下标
                int i = indexFor(e.hash, newCapacity);
              	//采用头插法将元素迁移到新数组中
                e.next = newTable[i];
                newTable[i] = e; 
                e = next;
            }
        }
    }

这段代码是JDK1.7中HashMap的扩容操作,重新定位每个桶的下标,采用头插法将元素迁移到新数组中。头插法会将链表的顺序反转,这是死循环的原因。

扩容引发的数据不一致问题

在JDK1.7出现的问题在JDK1.8中得到很好的解决,在1.8中没有了transfer方法,直接在resize()完成了数据迁移,而且元素插入采用尾插法。

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) // 如果没有hash碰撞则直接插入元素
            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;
    }
  • 第六行代码是判断是否出现hash碰撞,假设两个线程A和B都在进行put操作,并且hash函数计算出结果相同,当线程A执行完第六行代码时让出了CPU,线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所以此时不会再判断,而是直接插入,就会覆盖线程B的数据。
  • if ((e = p.next) == null)也会出现线程安全问题,当两个线程A和B都执行到这里,线程A和B都判断if ((e = p.next) == null)为真,此时线程A和B持有的e和p都是同一链表的节点,此时线程A挂起,线程B完成了插入,即p.next=new Node(hash,key,value,null),然后A获得时间片也执行了插入,就发生了覆盖。

源码分析

字段和构造函数

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

    //初始容量16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    //最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;

    //负载因子默认0.75
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    //桶上的链表长度大于等于8时,链表转化为红黑树
    static final int TREEIFY_THRESHOLD = 8;

    //桶上的红黑树大小小于等于6时,红黑树转化为链表
    static final int UNTREEIFY_THRESHOLD = 6;

    //当数组容量大于64时,链表才会转化为红黑树
    static final int MIN_TREEIFY_CAPACITY = 64;

    //链表的节点,一个哈希桶中的Node,hash和key可能都不相同,因为槽点是通过hash%(n-1)得到的。
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//当前node的hash值,作用是决定位于哪个哈希桶,是通过key的hashCode计算的
        final K key;//可能是包装类实例也可能是自定义对象,不同key可能有相同的hash
        V value;
        Node<K,V> next;//指向下一个node节点,构成单向链表。

        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;
        }
    }
  
		//存放数据的数组
    transient Node<K,V>[] table;

    //遍历的entrySet
    transient Set<Map.Entry<K,V>> entrySet;

    //HashMap的实际大小。注意两点:1、具体节点的数量并没有成员变量做记录,只是在每次遍历一个桶时用binCount作为局部变量计数。2、size可能不准,因为当你拿到这个值时,可能又发生变化。
    transient int size;

    //记录迭代过程的HashMap结构是否发生变化,如果有变化,迭代时会fail-fast
    transient int modCount;

    //阈值,两个作用:初始容量和扩容阈值
    int threshold;
    //负载因子,当元素容量达到了数组总容量的loadFactor,就自动扩容
    final float loadFactor;

    
    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;
        this.threshold = tableSizeFor(initialCapacity);
    }

    //使用传入的初始化容量和0.75
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    //使用16和0.75
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

    //使用已有的HashMap构建一个新的HashMap
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

capacity必须是2的幂

  • 目的:保证计算出的桶位置tab[i=(n-1) & hash]的结果在[0,Cap-1]之间。
  • 实现:空参构造:n=16,指定初始容量:通过threshold=tableForSize计算初始容量,得到>=cap的最接近2的幂,比如给定初始大小为19,则实际上初始化大小为32。
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;
        } 
  • 后续扩容resize>>1,即两倍扩容。

threshold的阈值

threshold有两个作用:

  • 初始容量:用于指定容量时的初始化,=tableDizeFor(initialCap),在resize()中使用。
  • 扩容阈值:=数组容量*0.75,在putValue()中使用,用于记录数组扩容阈值。

hash算法

计算每个Node的hash值,这个hash值是确定hash桶的依据。

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
  • 如果某个类型要作为hashMap的key,必须要有hashCode方法。
  • 将hashCode值再右移16位是为了让计算出的hash更分散,以减少哈希碰撞。
  • 得到hash值后,具体的槽点=hash % (length)=hash & (length-1)

put方法

public V put(K key, V value) {
  			//传入hash值的目的是定位目标桶
        return putVal(hash(key), key, value, false, true);
    }
//onlyIfAbsent:为true表示只在key不存在时添加,默认为false。返回的是oldValue,若不存在相同key返回null。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
  			//n为数组长度,i为数组下标索引,p为i下标位置的Node
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
          	//如果数组为空,使用resize()方法初始化
            n = (tab = resize()).length;
  			//根据hash值和数组长度n计算数组下标(n - 1) & hash其实就是hash%(n-1),如果当前索引位置为空,直接生成节点在当前索引位置上
        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))))
              //如果key的hash和值都相等,直接把当前下标的Node值赋值给临时变量
                e = p;
            else if (p instanceof TreeNode)
              	//如果是红黑树,使用红黑树的方式新增
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
              //走到这里,说明还是个链表,自旋,从链表头开始遍历,binCount还有记录链表长度的作用
                for (int binCount = 0; ; ++binCount) {
                  	//p.next==null表示已经遍历到链表尾
                    if ((e = p.next) == null) {
                      //尾插法
                        p.next = newNode(hash, key, value, null);
                      //插入之后当链表长度大于8要转成红黑树
                        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 = p.next
                    p = e;
                }
            }
          //说明链表中存在与key相同的结点
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
              //当onlyIfAbsent为false时,才会覆盖值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
              //返回旧值
                return oldValue;
            }
        }
  			//修改次数+1,用于fail-fast
        ++modCount;
  			//如果hashMap的实际大小大于扩容的阈值,开始扩容,注意这里是先增加再扩容
        if (++size > threshold)
            resize();
  			//对于LinkedListMap判断是否执行LRU条件之一(默认为true)
        afterNodeInsertion(evict);
        return null;
    }

链表树化&&向红黑树中添加元素

treeifyBin():链表转化为红黑树

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
  			//如果当前hash表长度小于64,不会树化而是进行扩容
        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);
        }
    }

树化的条件:

  • 链表长度大于等于8
  • 整个数组的大小大于64(当数组大小小于64时,只会进行扩容,不会树化) 链表长度阈值8是怎么得来的呢?
  • 在链表数据不多的时候,使用链表遍历较快,且红黑树占用空间是链表的两倍。
  • 参考泊松分布概率函数,由泊松分布得出结论,链表各个长度命中概率:当链表的长度为8时,出现的概率是0.00000006,不到千分之一,正常情况下,链表的长度不可能达到8,一旦达到8肯定是hash算法出现了问题。 putTreeVal()方法:
//h是key的hash值
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                       int h, K k, V v) {
            Class<?> kc = null;
            boolean searched = false;
  					//找到根节点
            TreeNode<K,V> root = (parent != null) ? root() : this;
  					//自旋,从根节点遍历
            for (TreeNode<K,V> p = root;;) {
                int dir, ph; K pk;
              	//p的hash值大于h,说明p在h的右边
                if ((ph = p.hash) > h)
                    dir = -1;
              	//p的hash值小于h,说明p在h的左边
                else if (ph < h)
                    dir = 1;
              	//要放进去的key在树中已经存在了,直接返回。
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                    return p;
              	//自己实现的Comparable的话,不能用hashCode比较了,需要用compareTo
                else if ((kc == null &&
                          //得到key的Class类型,如果key没有实现Comparable就是null
                          (kc = comparableClassFor(k)) == null) ||
                         //当前节点pk和入参k不等
                         (dir = compareComparables(kc, k, pk)) == 0) {
                    if (!searched) {
                        TreeNode<K,V> q, ch;
                        searched = true;
                        if (((ch = p.left) != null &&
                             (q = ch.find(h, k, kc)) != null) ||
                            ((ch = p.right) != null &&
                             (q = ch.find(h, k, kc)) != null))
                            return q;
                    }
                    dir = tieBreakOrder(k, pk);
                }

                TreeNode<K,V> xp = p;
              //找到和当前hashCode值接近的节点(当前节点的左右子节点其中一个为空即可)
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    Node<K,V> xpn = xp.next;
                  	//生成新的节点
                    TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
                  	//把新节点挡在当前子节点为空的位置上
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                  	//当前节点和新节点建立父子前后关系
                    xp.next = x;
                    x.parent = x.prev = xp;
                    if (xpn != null)
                        ((TreeNode<K,V>)xpn).prev = x;
                  /*
                  balanceInsertion:对红黑树进行着色或旋转
                  着色:新节点总是为红色,如果父节点是黑色则不需要重新着色,如果父节点是红色则需要重新着色或旋转再次达到红黑树的5个约束条件。
                  旋转:父亲是红色,叔叔是黑色时,进行旋转。如果当前节点是父亲的右节点,则进行左旋。如果当前节点是父亲的右节点,则进行右旋。
                  moveRootToFront是把算出来的root放到根节点上
                  */
                    moveRootToFront(tab, balanceInsertion(root, x));
                    return null;
                }
            }
        }

红黑树的5个原则

  • 节点是红色或黑色
  • 根节点是黑色
  • 所有叶子节点都是黑色
  • 从任意节点到每个叶子的所有简单路径都包含相同数目的黑色节点
  • 从每个叶子到根的所有路径上不能有连续的红色节点

扩容resize()函数

1、计算新数组大小

  • 初始化:指定容量,newCap = threshold,未指定容量,newCap=16
  • 扩容:newCap = oldCap * 2
    2、指定扩容:逐个遍历所有槽点
  • 当前槽点只有一个node,重新分配到新数组即可:newTable[e.hash & (newCap-1)]
  • 当前槽点下是红黑树
  • 当前槽点下是链表:因为链表中所有node的hash和key相同,而现在数组扩容了两倍,怎么把当前链等分成两部分呢?分成high链和low链,两链的关系近似理解为单双数节点。Head用来标识链首,Tail用来尾连接。low链置于newTab[j],high链置于newTab[j+oldCap](j表示原来数组位置)
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
  			//情况1,oldCa>0表示要扩容
        if (oldCap > 0) {
          	//当旧数组容量大于最大值,无法再扩容
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
          	//newCap=oldCap*2,newThr=oldThr*2
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
  			//情况2,初始化,且已指定初始化容量
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        //情况3,初始化,未指定初始化容量,则cap=16,threshold=16*0.75
  			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);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
  			//执行初始化,建立新数组
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  			//将新数组赋值给table
        table = newTab;
  			//oldTab != null表示扩容,初始化不走这里
        if (oldTab != null) {
          	//遍历原数组
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
              	//当原数组的当前槽点有值时,将其复制给e
                if ((e = oldTab[j]) != null) {
                  	//释放原数组的当前节点内存,帮助GC
                    oldTab[j] = null;
                  	//若当前槽点只有一个值,即链表只有一个节点,直接找到新数组响应位置并赋值
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                  	//若当前节点下是红黑树
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                  	//若当前节点下是链表
                    else { // preserve order
                      	//将链表分成两条链,low低位链和high高位链。Head标识链头,Tail用来连接
                        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);
                      	//将低位链放置于老链同一下标,例如原来当前节点位于oldTab[2],则现在低位链还处于newTab[2]
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                      //将高位链放置于老链下标+oldCap,例如原来当前节点为与oldTab[2],则现在高位链置于newTab[2+8=10]
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

ConcurrentHashMap

ConcurrentHashMap的出现主要为了解决HashMap在多线程环境下的不安全,JDK1.8设计巧妙,大量利用了volatile和CAS等乐观锁机制减少锁竞争对于性能的影响,ConcurrentHashMap保证线程安全的方案是:

  • Java8:synchronized+CAS+HashEntry+红黑树
  • Java7:ReentrantLock+Segment+HashEntry

Java7的ConcurrentHashMap

在jdk1.7中是由分段锁Segment数据结构+HashEntry数组组成,且主要通过Segment分段锁技术实现线程安全。Segment是一种可重入锁,是一种数组和链表的结构,一个Segment包含一个HashEntry数组,每个HashEntry又是一个链表结构,因此ConcurrentHashMap查询一个元素需要两次Hash过程。

  • 第一次hash定位到Segment
  • 第二次hash定位到元素所在的链表的头部

image.png

通过Segment分段锁技术,将数据分成一段段存储,然后给每段分配一把锁,当一个线程访问其中一段时,其他段的数据也能被其他线程访问,实现段之间的并发访问。这样的结构使得hash过程变长,影响读性能,但写操作可以并发。

Java8的ConcurrentHashMap

JDk8的ConcureentHashMap的内部结构是:数组+链表+红黑树,Java8在链表长度超过一定值(默认是8)会转换成红黑树,跟hashMap一样。

image.png

源码分析

Node类

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;  //使用了volatile属性
        volatile Node<K,V> next;  //使用了volatile属性
        ...
    }

ConcurrentHashMap采用Node作为基本的存储单元,每个键值对都存储在一个Node中,使用volatile修饰next和value,保证可见性。 ConcurrentHashMap中查找元素、替换元素和赋值元素都是基于sun.misc.Unsafe中原子操作实现多并发的无锁化操作。

static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }

    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }

    static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
    }

put方法

put方法添加一个键值对封装成Node对象到ConcurrentHashMap中,通过key的hashCode计算出一个数组下标索引用来存储。

public V put(K key, V value) {
        return putVal(key, value, false);
    }
final V putVal(K key, V value, boolean onlyIfAbsent) {
  			//空值校验
        if (key == null || value == null) throw new NullPointerException();
  			//计算key的hash值
        int hash = spread(key.hashCode());
        int binCount = 0;
  			//遍历table
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
          	//table没有初始化则进行初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
          	//计算要存储的索引位置i = (n - 1) & hash,跟HashMap实现一样。索引位置元素为空,则尝试CAS设置Node
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
          	//当前正在扩容,帮助扩容迁移
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
          	//存储的位置hash碰撞了
            else {
                V oldVal = null;
              //锁定table[i]桶中的头结点
                synchronized (f) {
                  	//二次校验头结点是否变化
                    if (tabAt(tab, i) == f) {
                      //当前桶中的冲突时链表结构
                        if (fh >= 0) {
                            binCount = 1;//计算链表元素个数,用于判断是否要转成红黑树
                          	//遍历链表
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                              	//找到key相同的节点,如果onlyIfAbsent=false就覆盖原值
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                              	//没有相同的key就生成一个新的节点插入链表尾端
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                      	//如果头结点是红黑树结构,putTreeVal将节点加入到红黑树
                        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;
                            }
                        }
                    }
                }
              	//根据table[i]桶node个数是否超过TREEIFY_THRESHOLD阈值进行红黑树转换
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
  			//size计算以及判断是否扩容
        addCount(1L, binCount);
        return null;
    }

put方法需要注意的几点:

  • 如何解决的hash碰撞:当出现hash冲突时,也就是计算出的table[i]已经有node了,通过链表和红黑树两种方式处理。
  • 如何解决多线程并发安全问题: (1)在第一次put数据的时候,调用initTable()方法,使用sizeCtl参数作为控制标志,当插入元素时才会初始化Hash表,在开始初始化时,先判断sizeCtl,如果小于0表示有线程正在初始化,当前线程放弃初始化操作。否则将sizeCtl设置为-1,Hash表进行初始化。初始化成功以后,将sizeCtl的值设置为当前的容量值。
//Hash表的初始化和调整大小的控制标志。为负数表示Hash表正在初始化或正在扩容。(-1表示正在初始化,-N表示N-1个线程在进行扩容),否则,当表为null时,保存创建时使用的初始化大小或者默认0。初始化以后保存下一个调整大小的尺寸。
private transient volatile int sizeCtl;

//第一次put,初始化数组
private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
          	//如果已经有别的线程在初始化,这里等待一下
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
          	//-1表示正在初始化
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

(2)判断在table[i]位置没有node时会尝试CAS原子性插入 (3)table[i]位置已经有node了,会锁定头结点,synchronized(table[i])来操作链表或红黑树,通过锁定头结点的方式来确保线程安全,所有节点都是尾插,头结点不变。

  • 多线程扩容实现:(fh==f.hash)==MOVED时会辅助扩容,而且当插入完成之后addCount(1L,binCount)还会判断是否需要扩容。
  • 计算hash值,通过hash算法可以将元素分散到哈希桶中。
static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }

今天就到这里了,不知不觉已经一万字了..