真就为了HashMap,千千万万遍???看完这篇博客,一遍就行!!

「这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战


1. Collections

1.1 List

特点:

  1. 有序(指的是存取有序)
  2. 可重复
  3. 可通过索引值操作元素

分类:

  • 底层是数组,查询快,增删慢;(ArrayList,线程不安全,效率高;Vector,线程安全,效率低;数组在内存中连续存储,能够通过索引快速访问)
  • 底层是链表,查询慢,增删快;(LinkedList,线程不安全,效率高;链表在内存中不是连续存储的,存储值的时候也要存储指向下一个值的指针,相比于数组占用了更多的内存空间,但是它并不存在扩容问题)

源码:

  • 扩容方法,grow()
    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);
    }
复制代码

1.2 Set

特点:

  1. 无序
  2. 元素唯一

分类

  • 底层是HashMap;(HashSet,保证元素的唯一性,利用的是hashCode()和equals()方法)

  • 底层是TreeMap;(TreeSet,保证元素的有序性

  • 进行的排序方式

  1. 对象所属的类自己实现comparable接口,向TreeSet中添加元素的时候,会调用compareTo()方法比较
  2. 在创建TreeSet对象的时候,构造函数中传入comparator(),源码如下
    public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }
复制代码

2. HashMap

在JDK1.8之前,底层是数组+链表 在JDK1.8,底层是数组+链表+红黑树,下面主要介绍JDK1.8中的HashMap

2.1 几个简单的参数

//初始化大小为16,左移运算符<<,移动一位*2
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

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

//负载因子,默认0.75,不需要修改,这是经过实践得出的最合适的
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//由链表变成数的阈值
static final int TREEIFY_THRESHOLD = 8;

//由树变回链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
复制代码

2.2 构造函数

	//无参构造,大小为16,负载因子为0.75,不进行初始化,懒加载
	//后边put()方法中说明
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; 
    }
 
 	//修改负载因子的构造函数,初始大小为16,调用下方的构造函数
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    
    //
    public HashMap(int initialCapacity, float loadFactor) {
    	//初始容量不能小于0
        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;
        //初始容量准备好了,利用tableSizeFor()方法来计算table的阈值
        //注意,并不进行初始化,只有在第一次put的时候才进行初始化
        this.threshold = tableSizeFor(initialCapacity);
    }
复制代码

下面我们看一下tableSizeFor()方法,计算table大小的阈值

    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;
    }
复制代码

我们来图解一下这个过程,以65为例 在这里插入图片描述

2.3 put()方法源码

  • 方法逻辑
  1. 如果HashMap未被初始化,则进行初始化
  2. 对Key求Hash值,然后再计算下标
  3. 如果没有碰撞了,直接放入桶中
  4. 如果碰撞了,以链表的方式链接到后面
  5. 如果链表长度超过8,就把链表转成红黑树
  6. 如果链表长度低于6,就把红黑树转回链表
  7. 如果节点已经存在,就替换旧值
  8. 如果桶满了(超过容量*0.75),就需要resize,扩容2倍后重排
  • 源码
   public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
	
	 /**
     * Implements Map.put and related methods.
     *
     * @param hash key的hash值
     * @param key key值
     * @param value value值
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //tab代表Node数组,p为数组中已存在的Node,n为数组长度,i为索引值
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //Node[]为空或者长度为0,当我们第一次进行put()的时候就是这样
        //通过resize()方法进行初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        
        //不发生碰撞的情况下,直接放入桶中
        //计算索引采用的是(长度-1)与hash值进行位与运算
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
        	//这里是发生碰撞的情况
            Node<K,V> e; K k;
            //已存在的node的hash值和加入的hash值相等
            //且key一致
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
            	//若新加入的node为树的节点的话,调用的是putTreeVal()方法
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            	//发生碰撞的时候,不是头节点的key一致,那么要对链表进行遍历寻找
                for (int binCount = 0; ; ++binCount) {
                    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))))
                        //在这里找到了hash值相同的key的链表位置
                        break;
                    p = e;
                }
            }
            //加入的节点不是空节点,且e已经到了key所在的链表位置
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                //这里就是为什么我们插入成功的时候会返回旧值
                return oldValue;
            }
        }
        //操作计数+1
        ++modCount;
        //若超过大小*0.75的阈值,需要进行扩容重排
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
复制代码

2.3.1 resize()方法源码

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        //容量为零,这里供调用无参构造就行初始化的时候使用
        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)
                //符合扩容条件,让阈值*2
                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);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    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
                        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;
                        }
                    }
                }
            }
        }
        return newTab;
    }
复制代码

2.3.2 扩容存在的问题

  • 在多线程环境下,调整大小会存在条件竞争,造成死锁
  • rehashing是一个比较耗时的过程

2.3.3 HashMap如何减少碰撞?

  • 扰动函数:促使元素位置分布更加均匀,减少碰撞几率
  • 使用被final修饰的对象,并采用合适的equals()和hashcode()方法

2.4 get()方法源码

	//我们调用的get()方法
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

	//内部实际调用的get()方法
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                //总会定位到数组中链表第一个头节点的位置
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                	//是树节点的时候调用getTreeNode()方法
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                	//比较hash值和key
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
复制代码

2.5 hash()源码

    static final int hash(Object key) {
        int h;
        //如果key为null,把它放在数组的第一个位置,HashMap是可以存null的
        
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
复制代码

若key不为null时,我们对其进行图解 在这里插入图片描述

3. ConcurrentHashMap

3.1 让HashMap线程安全的方法

  1. 使用Collections.sychronizedMap(hashMap)方法
  2. HashTable是线程安全的,因为它的public方法都被sychronized修饰
  3. 使用ConcurrentHashMap

3.2 ConcurrentHashMap的底层原理

  • 早期的ConcurrentHashMap是通过分段锁segement来实现,segement继承ReetrantLock,每个segement守护若干个entry
  • JDK1.8,CAS+sychronized使锁更加细化,CAS(比较并交换)是CPU指令级的操作,只有一步原子操作,所以非常快

3.3 重要概念

  • sizeCtl:默认为0,用来控制table初始化和扩容的操作

-1代表进行resize(),初始化或扩容重排 -n代表有n-1个线程正在进行resize()操作 若table未初始化,则代表需要初始化的大小 若table初始化完成,表示table的容量

private transient volatile int sizeCtl;
复制代码
  • Node

它的value和next都是volatile修饰的,保证并发的可见性

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

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

        public final K getKey()       { return key; }
        public final V getValue()     { return val; }
        public final int hashCode()   { return key.hashCode() ^ val.hashCode(); }
        public final String toString(){ return key + "=" + val; }
        public final V setValue(V value) {
            throw new UnsupportedOperationException();
        }

        public final boolean equals(Object o) {
            Object k, v, u; Map.Entry<?,?> e;
            return ((o instanceof Map.Entry) &&
                    (k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
                    (v = e.getValue()) != null &&
                    (k == key || k.equals(key)) &&
                    (v == (u = val) || v.equals(u)));
        }
复制代码

3.4 Table初始化

  • 源码
    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
            	//表示其他线程正在进行操作,当前线程要让出cpu时间片
                Thread.yield(); // lost initialization race; just spin
            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;
    }
复制代码

sizeCtl默认值为0,只有我们在实例化ConcurrentHashMap时传参的时候,sizeCtl会调用tableSizeFor()方法,赋值为一个2的n次幂的值。第一次执行put方法时,其中U.compareAndSwapInt(this, SIZECTL, sc, -1)会将其修改为-1,这就代表只能有一个线程能对其进行修改,其他线程则Thread.yield(),让出CPU时间片。

3.5 put()方法

  • 源码
    public V put(K key, V value) {
        return putVal(key, value, false);
    }

    final V putVal(K key, V value, boolean onlyIfAbsent) {
    	//不能添加null值
        if (key == null || value == null) throw new NullPointerException();
        //hash算法
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
            	//进行初始化
                tab = initTable();
            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)
            	//此时hash值为-1,表示当前f是ForwardingNode节点
            	//表示正在进行扩容操作,那么它要一起进行扩容
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {
                	//其他情况,采用的是同步内部锁保证并发
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                        	//表示f是链表的头节点
                            binCount = 1;
                            //遍历链表,若找到对应的节点,修改value值
                            //若没有找到,则在尾部进行添加
                            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;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                	//对转换为红黑树的阈值进行判断,大于8则换成红黑树
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }
复制代码

3.6 hash算法

与HashMap有些区别,多了一步和HASH_BITS进行位与运算

    static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }
复制代码

3.7 get()方法

    public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }
复制代码

get()方法比较简单,我们重点看一下其中的tabAt(tab, (n - 1)方法

    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);
    }
复制代码

U.getObjectVolatile(),保证每次都能获取到最新的数据,因为每个线程都有一个自己的工作内存,里边存有table的副本,为了保证它能每次都能从主内存中获得最新的值,所以会用此方法

3.8 扩容

  • 步骤
  1. 构建一个nextTable,大小为table的两倍
  2. 把table中的数据复制到nextTble中
  • 源码
    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();
            }
        }
    }
复制代码

通过Unsafe.compareAndSwapInt修改sizeCtl值,保证只有一个线程能够初始化nextTable,扩容后的数组长度为原来的2倍,但是容量是原来的1.5倍

节点从table移动到nextTable,大体思想是遍历、复制的过程。

  1. 首先根据运算得到需要遍历的次数i,然后利用tabAt方法获得i位置的元素f,初始化一个forwardNode实例fwd。

  2. 如果f == null,则在table中的i位置放入fwd,这个过程是采用Unsafe.compareAndSwapObjectf方法实现的,很巧妙的实现了节点的并发移动。

  3. 如果f是链表的头节点,就构造一个反序链表,把他们分别放在nextTable的i和i+n的位置上,移动完成,采用Unsafe.putObjectVolatile方法给table原位置赋值fwd。

  4. 如果f是TreeBin节点,也做一个反序处理,并判断是否需要untreeify,把处理的结果分别放在nextTable的i和i+n的位置上,移动完成,同样采用Unsafe.putObjectVolatile方法给table原位置赋值fwd。

遍历过所有的节点以后就完成了复制工作,把table指向nextTable,并更新sizeCtl为新数组大小的0.75倍 ,扩容完成。

4. HashTable、HashMap、ConcurrentHashMap的区别

  1. HashMap线程不安全,底层是数组+链表+红黑树
  2. HashTable线程安全,它锁住的是整个对象,底层是数组+链表
  3. ConcurrentHashMap线程安全,利用的是CAS+synchronized,底层是数组+链表+红黑树

猜你喜欢

转载自juejin.im/post/7033689753593577480