HashMap、HashTable、ConcurrentHashMap三者的区别

第一章 先分别说明

1.1 HashMap

1.1.1 HashMap介绍
  • 可以存储null键和null值,线程不安全
  • 在JDK1.7之前HashMap是由数组+链表构成的
  • 在JDK1.8之后HashMap则由数组+链表+红黑树构成
  • 初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
  • 扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
  • 插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
  • 当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
  • 计算index方法:index = hash & (tab.length – 1)
1.1.2 HashMap的初始值还要考虑加载因子
  • 哈希冲突:若干Key的哈希值按数组大小取模后,如果落在同一个数组下标上,将组成一条Entry链,对Key的查找需要遍历Entry链上的每个元素执行equals()比较。
  • 加载因子:为了降低哈希冲突的概率,默认当HashMap中的键值对达到数组大小的75%时,即会触发扩容。因此,如果预估容量是100,即需要设定100/0.75=134的数组大小。
  • 空间换时间:如果希望加快Key查找的时间,还可以进一步降低加载因子,加大初始大小,以降低哈希冲突的概率。
1.1.3 HashMap源码

继承机制:

/*
 * @see     Collection
 * @see     Map
 * @see     TreeMap
 * @see     Hashtable
 * @since   1.2
 */
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

    private static final long serialVersionUID = 362498820763181265L;
}

扩容机制:

 /**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
    final Node<K,V>[] resize() {
        //初始化的table赋值
        Node<K,V>[] oldTab = table;
 
        //判断table是否初始化了,如果没有初始化则oldCap置为1,否则是初始化的长度
        //put前的容量
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
 
        //threshold-阈值,超过阈值会进行扩容 初始阈值为0
        //put前的阈值
        int oldThr = threshold;
 
        int newCap, newThr = 0; 定义新容量新阈值
 
 
        //put前已经有初始容量
        if (oldCap > 0) {
            //如果put前容量 >= 最大容量2的30次幂
            if (oldCap >= MAXIMUM_CAPACITY) {
 
                //阈值扩大到最大的int值 2^31-1
                threshold = Integer.MAX_VALUE;
 
                return oldTab;//返回put前的node[]
            }
            //如果是之前的容量扩大到2倍之后还小于map最大容量 并且  之前的容量超过初始容量16
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //新的阈值 为 老的阈值的两倍
                newThr = oldThr << 1; // double threshold
        }
        
        //put前的阈值大于0
        else if (oldThr > 0)
            newCap = oldThr;  // 初始容量被置于阈值
        else {               
            //零初始阈值表示使用默认值
            //16 初始容量
            newCap = DEFAULT_INITIAL_CAPACITY;
            //(0.75*16)初始阈值
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
 
        //新阈值为0
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            //新阈值=如果新容量<最大容量 并且阈值也小于最大容量则为当前容量*加载因子否则为最 
            //大的int值
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //修改当前map阈值为新阈值
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"}) //禁止显示警告
            //new一个新的长度为新容量的node[]
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
 
        //修改当前map中node[]
        table = newTab;
 
        //老的table存在
        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;
                        }
                    }
                }
            }
        }
        //返回这个重新构建的node[]
        return newTab;
    }

1.2 HashTable

1.2.1 HashTable介绍
  • 底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
  • 初始size为11,扩容:newsize = olesize*2+1
  • 计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length
1.2.2 HashTable源码

继承机制:

/*
 * @see     Hashtable#rehash()
 * @see     Collection
 * @see     Map
 * @see     HashMap
 * @see     TreeMap
 * @since JDK1.0
 */
public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable {
}

扩容机制:


protected void rehash() {
        int oldCapacity = table.length;
        Entry<?,?>[] oldMap = table;
 
        // 新数组的容量=旧数组长度*2+1
        int newCapacity = (oldCapacity << 1) + 1;
        // 保证新数组的大小永远小于等于MAX_ARRAY_SIZE
        // MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            if (oldCapacity == MAX_ARRAY_SIZE)
                return;
            newCapacity = MAX_ARRAY_SIZE;
        }
        // 创建新数组
        Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
 
        modCount++;
        // 计算新的临界值
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        table = newMap;
 
        // 将旧数组中的元素迁移到新数组中
        for (int i = oldCapacity ; i-- > 0 ;) {
            for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
                Entry<K,V> e = old;
                old = old.next;
 
                //计算新数组下标
                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                // 头插法的方式迁移旧数组的元素
                e.next = (Entry<K,V>)newMap[index];
                newMap[index] = e;
            }
        }


1.3 ConcurrentHashMap

锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

1.3.1 ConcurrentHashMap介绍
  • jdk1.7底层采用分段的数组+链表实现,线程安全
  • jdk1.8底层采用分段的数组+链表+红黑树实现,线程安全
  • 通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
  • Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
  • 读取数据时不加锁,高效,且因为map中的value值是添加volatile关键字修饰的,可保证读取到最新值,降低CPU负载。
  • 有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
  • 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容
1.3.2 ConcurrentHashMap源码

继承机制:

 /*
 * @since 1.5
 * @author Doug Lea
 * @param <K> the type of keys maintained by this map
 * @param <V> the type of mapped values
 */
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable {
    private static final long serialVersionUID = 7249069246763182397L;
}

扩容机制:

    private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        //【第一步】
        //决定当前线程在需要处理的槽位充足下,分配到的槽位数
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        //新容器为空则创建容器    
        if (nextTab == null) {            // initiating
            try {
                //多出一个赋值操作,尝试处理内存溢出?不明白原理
                @SuppressWarnings("unchecked")
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab;
            //转移索引数设置为当前容器容量
            transferIndex = n;
        }
        //将下个容器的转移搜索引数设置为新容器容量
        int nextn = nextTab.length;
        //创建ForwardingNode容器并放入新容器
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        boolean advance = true;
        boolean finishing = false; // to ensure sweep before committing nextTab
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            //【第二步,划分槽位,帮助推进】
            //选择当前线程进行transfer的槽位,从最后一个槽位向前
            while (advance) {
                int nextIndex, nextBound;
                //向前推进一个槽位,或者已经完成了
                if (--i >= bound || finishing)
                    advance = false;
                //槽位被其它线程选择完了    
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                //尝试获取槽位的操作权
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    //槽位下限                   
                    bound = nextBound;
                    //当前选中进行处理的槽位
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            //被选择完毕,选中槽位大于当前容器容量,选中槽位+当前容器容量大于新容器容量
            //【第三步,设置结束条件,变更地址】
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                //扩容完毕
                if (finishing) {
                    //清除扩容时创建的临时表
                    nextTable = null;
                    //将当前表指向临时表
                    table = nextTab;
                    //设置下次扩容的临界点为 0.75*扩容容量
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                //将扩容标识中的线程标识减一
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    //存在其它线程进行扩容处理,则当前线程处理完自己的槽位后直接退出
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    //不存在其它线程处理,说明自己是唯一处理线程   
                    finishing = advance = true;
                    //将i重置,在看下还有没有transferIndex
                    //如果已经是唯一处理线程并且满足前置条件,为何需要检查下?
                    i = n; // recheck before commit
                }
            }
            //【第四步,处理槽位】
            //如果当前槽中没有成员,用forwarding节点占位
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            //如果当前槽中成员为forwarding节点,代表已经被处理过了    
            else if ((fh = f.hash) == MOVED)
                //处理下一个槽
                advance = true; // already processed
            else {
                //锁住槽位
                synchronized (f) {
                    //double check
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                        if (fh >= 0) {
                            //计算当前成员最高位
                            //runBit是0 or 1
                            int runBit = fh & n;
                            Node<K,V> lastRun = f;
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                //查找最后重复的链,获得开始位置p,和重复的高位值runBit
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            //如果从p开始后面高位全是0,那么就不需要移动到新槽中
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            //如果从p开始后面全是1,那么就需要移动到新槽中
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            //从链的头部一直遍历到p的位置(因为p以后高位都一样)
                            //为何需要提前找一部分重复?效率更高?这么处理是否有理论依据?
                            for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                //高位为0放到旧槽位中
                                if ((ph & n) == 0)
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                //高位为1放到新槽位中
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            //将ln放到新容器的旧槽位中
                            setTabAt(nextTab, i, ln);
                            //将hn放到新容器的新槽位中
                            setTabAt(nextTab, i + n, hn);
                            //将老容器中的该节点设置为forwarding节点
                            setTabAt(tab, i, fwd);
                            //处理下一个槽位
                            advance = true;
                        }
                        //TreeBin的hash固定为-2,红黑树的调整
                        else if (f instanceof TreeBin) {
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> lo = null, loTail = null;
                            TreeNode<K,V> hi = null, hiTail = null;
                            int lc = 0, hc = 0;
                            for (Node<K,V> e = t.first; e != null; e = e.next) {
                                int h = e.hash;
                                TreeNode<K,V> p = new TreeNode<K,V>
                                    (h, e.key, e.val, null, null);
                                if ((h & n) == 0) {
                                    if ((p.prev = loTail) == null)
                                        lo = p;
                                    else
                                        loTail.next = p;
                                    loTail = p;
                                    ++lc;
                                }
                                else {
                                    if ((p.prev = hiTail) == null)
                                        hi = p;
                                    else
                                        hiTail.next = p;
                                    hiTail = p;
                                    ++hc;
                                }
                            }
                            //槽位里成员少于等于6,退化为链表
                            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                (hc != 0) ? new TreeBin<K,V>(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                (lc != 0) ? new TreeBin<K,V>(hi) : t;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                    }
                }
            }
        }
    }

第二章 总体来分析三者(一个表格)

HashMapHashTableConcurrentHashMap
null键允许不允许不允许
null值允许不允许不允许
效率非常高
线程安全不安全安全安全
数据结构jdk1.8 数组+链表+红黑树数组+链表jdk1.8 数组+链表+红黑树
同步方式synchronized同步方法1.7版本:基于segment分段锁机制,基于ReentrantLock实现;1.8版本:基于CAS+synchronized实现,空节点插入使用CAS,有Node节点则使用synchronized加锁
迭代器类型fail-fast迭代器:在遍历时不断更新元素,否则将抛出异常fail-safe迭代器:基于容器的克隆,因此遍历操作时元素的更新不影响遍历fail-safe迭代器:基于容器的克隆,因此遍历操作时元素的更新不影响遍历

总结:不涉及线程安全问题时使用HashMap,要保证线程安全时,使用ConcurrentHashMap。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值