jdk1.7 HashMap&ConcurrentHashMap分析

 1.7HashMap分析

一、基本了解       

       HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。HashMap 实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步。HashMap 是无序的,即不会记录插入的顺序。HashMap 继承于AbstractMap,实现了 Map、Cloneable、java.io.Serializable 接口。HashMap 继承于AbstractMap,实现了 Map、Cloneable、java.io.Serializable 接口。

        1.7中HashMap结构是:数组+散列表。存储的是Entry<K,V>对象:

我们可以看到Entry<K,V>对象是HashMap的内部类,他包含4个属性,其中Entry<K,V> next是链表中指向下个Entry<K,V>节点

二、源码分析

当我们new一个HashMap时,可以传两个参数或者不传(如果不传的话默认初始大小是16,加载因子0.75)

我们可以看看这个构造方法:

put<K,V>方法分析

 public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);//计算key值得hash值
        int i = indexFor(hash, table.length);//通过hash值和数组长度计算该key值的数组下标
        //从table[i]处遍历链表,有重复的key则替换并返回老value值
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        //没有重复的key则添加新的Entry<K,V>对象
        addEntry(hash, key, value, i);
        return null;
    }

我们可以看到,如果key==null,调的是putForNullKey(value),这个方法时在计算key值的hash之前调用的

数组的大小为什么要设计成2的幂次方呢?我们可以看下计算数组下标i的方法

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

首先,HashMap设计的初衷就是让存储时时随机的,这一点hash算法已经帮我们保证了。第二点,我们要让算出来数组下标在我们容量范围内。这里就以初始容量16为例。int类型是32bit位,我们这就简单的用8位表示

16:0000  1000 ---2的幂次方

h: 0101  0101(hash值是随机的,可以随便写)经过和15&运算后

&

15:0000 1111 ---2的幂次方减一 低4位永远都是四个一

 结果: 0000 0101(&运算后低四位是和hash一模一样的,所以范围就是0000-1111)

我们可以发现,不过hash值是什么,经过&运算得出的结果永远在0-15我们的数组范围内,这就是HashMap保证数组大小是2的幂次方

当然,为了让key计算的hash值更加的散列,让高位也参加进来计算,前面这个方法有进行一系列的右移操作和^运算

 final int hash(Object k) {
        int h = 0;
        if (useAltHashing) {
            if (k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h = hashSeed;
        }

        h ^= k.hashCode();

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

该方法可以看出,key==null的值存在table[0]的位置

我们再看下addEntry()添加新的Entry<K,V>对象这个方法

 void addEntry(int hash, K key, V value, int bucketIndex) {
        //size超过阈值且当前位置不为空
        if ((size >= threshold) && (null != table[bucketIndex])) {
            //扩容处理
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        //创建新的Entry对象
        createEntry(hash, key, value, bucketIndex);
    }

再看下createEntry()方法

 void createEntry(int hash, K key, V value, int bucketIndex) {
        //这里可以看出采取的头插法
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

这里会产生线程不安全,假设有两个线程T1,T2一起执行put()操作,key算出的数组下标都是一样的。都跑到了createEntry()方法,此时T2线程被挂起(为分到CPU时间片),这是T1线程把新的Entry对象已经赋值给数组,这是T2获取到执行时间,就会重新对该位置赋值,把T1线程的值覆盖,造成数据丢失。

接下来我们看看扩容方法的源码resize(int newCapacity)

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        //如果老表数组已经是最大值,则不扩容直接返回
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        //useAltHashing默认是false
        boolean oldAltHashing = useAltHashing;
        //当我们新数组大小大于 Holder.ALTERNATIVE_HASHING_THRESHOLD,这个结果为true,则rehash为true,重新计算hash
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;
        //老表数据迁移到新表
        transfer(newTable, rehash);
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

所以我们来看看Holder这个静态内部类

 private static class Holder {
        /**
         * Table capacity above which to switch to use alternative hashing.
         */
        static final int ALTERNATIVE_HASHING_THRESHOLD;

        static {
            //获取本地变量jdk.map.althashing.threshold值
            String altThreshold = java.security.AccessController.doPrivileged(
                new sun.security.action.GetPropertyAction(
                    "jdk.map.althashing.threshold"));

            int threshold;
            try {
                threshold = (null != altThreshold)
                        ? Integer.parseInt(altThreshold)
                        : ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;//为null取Integer最大值

                // disable alternative hashing if -1
            ALTERNATIVE_HASHING_THRESHOLD = threshold;
           
        }
    }

也就是说Hashmap给我们提供了这样一个功能,我们可以去配置这个变量的值,当我们扩容的新数组大于jdk.map.althashing.threshold,则重新计算hash,不配这个值则默认是Integer的最大值即2^31-1,所以一般我们不配扩容时都不需要rehash。1.8中已经去掉这个判断,扩容时不需要rehash。

我们再看下老表数据迁移到新表方法transfer(newTable,rehash)

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赋值
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                //这里算数组下标的方法和老表算是一样
                int i = indexFor(e.hash, newCapacity);
                //头插法,循环遍历完后链表顺序相反
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

我们可以看到,如果rehash为false(1.8中这个判断直接取消了),计算新数组下标的方法和老表是一样的,前面我们有举例子

16:0000  1000 ---2的幂次方                                                                  翻倍以后——> 0001  0000

h: 0101  0101(hash值是随机的,可以随便写)经过和15&运算后                              0100 0101或0101 0101

&

15:0000 1111 ---2的幂次方减一 低4位永远都是四个一                                          31:0001 1111    运算结果就是0000 0101(原来的数组下标) 或者 0001 0101(原来的数组下标+oldtable.length)

扩容的目的是为了让链表的长度变短,提高get()方法效率,因为扩容以后数据迁移到新table上的位置是两个(原来数组的下表 或 原来的数组下标+oldtable.length)

resize()扩容这里也会造成线程不安全,会造成一个死循环,主要是迁移这个方法transfer(newTable,rehash)造成的,采取了头插法。具体可以参照这边文章:https://juejin.cn/post/6844903554264596487

三、容错机制:fast-fail(快速失败)

在上面源码的put(K,V)方法中,我们发现,我们每put()一次modCount++,这个属性代表:已对HashMap进行结构修改的次数。

这里有一段代码

 public static void main(String[] args) {
        HashMap<String,String> map = new HashMap<String, String>();
        map.put("1","1");
        map.put("2","2");

        for (String key: map.keySet()) {
            if(key.equals("1")) {
                map.remove(key);
                System.out.println("remove key1 success");
            }
            if(key.equals("2")){
                map.remove(key);
                System.out.println("remove key2 success");
            }
        }

执行结果是:

为什么会出现这种情况呢:为了更好分析,我们拿编译过后的代码分析:

//编译之后的代码  fast-fail(快速失败,容错机制)
        HashMap<String, String> map = new HashMap(); //expectedModCount=2  modCount(代表修改)=2  不管在put()或remove操作时:modCount++
        map.put("1", "1");
        map.put("2", "2");
        Iterator i$ = map.keySet().iterator();

        while(i$.hasNext()) {
            String key = (String)i$.next();
            if (key.equals("1")) {
                map.remove(key);//modCount++
                //i$.remove(); 这里会重新 设置 expectedModCount=modCount
                System.out.println("remove key1 success");
            }

            if (key.equals("2")) {
                map.remove(key);//modCount++
                //i$.remove();这里会重新 设置 expectedModCount=modCount
                System.out.println("remove key2 success");
            }

        }
为什么这样设计的。主要是在多线程情况下,假设两个线程T1,T2。T1在遍历,T2在修改,就会抛出异常。单线程下避免报错可以调用迭代器的remove()方法

 1.7ConcurrentHashMap分析

ConcurrentHashMap是线程安全的,它采取的是分段式锁,它的数据结构还是数组+链表。只不过数组放的不是Entry<K,V>对象,而是Segment<K,V>[]数组,(我们可以理解每个Segment就是一个HashMap(线程不安全),只是他是线程安全的,因为Segment继承了ReentrantLock)而Segment里放的是HashEntry<K,V>对象(与HashMap中Entry<K,V>一样)。ConcurrentHashMap中key,value都不能为null。

 

当我们不传参数创建一个new ConcurrentHashMap(),会默认传三个参数

initialCapacity:初始化容量(默认16),内部会作调整一容纳更多元素。   loadFactor:加载因子(0.75)。  concurrencyLevel:并发级别(默认16)。 一个segment就一个锁。当我们初始化完后,Segment<K,V>[]数组长度不变了,只有HashEntry<K,V>[]数组长度会变(扩容)

假设我们new ConcurrentHashMap(33,0.75f,16),由源码得知,Segment[ssize]大小为16,HashEntry[cap]大小为4,

这里是因为put()方法中算key的hash和HashMap中一样,第一步算出Segment[]数组下标:hash&Segment[].length-1(拿的是高四位进行&计算)。第二部算出HashEntry[]数组下标:hash&HashEntry[].length-1(和HashMap一样,用的是低四位)。前面我们分析过,只有数组大小为2的幂次方算出来的下标才会刚好在数组大小范围内。所以这里和HashMap一样,两个数组大小初始化是都会处理成2的幂次方。

上面还有这段代码很重要:

这里创建了一个Segment<K,V> S0 = new Segment<K,V>(a,b,c);这三个参数分别为加载因子、阈值(超过了扩容,segment内部扩容)、HashEntry对象。并调用UNSAFE(CAS算法保证线程安全只会PUT一个s0)方法把s0放到ss[0]的位置,然后其他Segment<K,V>[]位置都为null。为什么这样设计呢?

因为当我们调用ConcurrentHashMap的put()方法时,假如通过key算出下标为1,此处Segment<K,V>[]为null,我们只需要把S0位置上的Segment<K,V>拷贝过来就行,S0那三个参数已经在构造器中初始化好了,然后再调用Segment<K,V>中的put()方法。接下来我们就来看看ConcurrentHashMap中put()方法源码

我们看下是如何生成Segment<K,V>对象的:

Segment<K,V>[i]中的Segment<K,V>对象创建好以后,调用Segment<K,V>中的put()方法开始进行真正的put()操作

如果获取到则返回null,如果没获取到则走scanAndLockForPut(),我们点进该方法

tryLock():非阻塞  T1线程尝试去获取锁,获取到则返回true,没获取到则false(比如被其他线程占有),T1线程不会阻塞可以去干其他事。

lock():阻塞   T1线程调用lock()方法,获取不到锁(比如被其他线程占有),则阻塞,一直等待获取锁

我们再来看看scanAndLockForPut(key,hash,value)这个方法:这个方法只是为了加锁。但在加锁过程中遍历链表提前new HashEntry(),但有点多余,因为后面也有去遍历链表。1.8中就去掉了这段逻辑

 private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
            //通过hash算出当前位置链表头结点HashEntry对象
            HashEntry<K,V> first = entryForHash(this, hash);
            HashEntry<K,V> e = first;
            HashEntry<K,V> node = null;
            int retries = -1; // negative while locating node
           //尝试去获取锁,当前线程获取失败则不会阻塞,继续执行下面代码。即根据判断是否要     
           //提前new HashEntry()对象
            while (!tryLock()) {
                HashEntry<K,V> f; // to recheck first below
                if (retries < 0) {
                    if (e == null) {
                        //需要new 一个新节点  
                        if (node == null) // speculatively create node
                            node = new HashEntry<K,V>(hash, key, value, null);
                        retries = 0;
                    }
                    //key有重复,不需要new一个新节点
                    else if (key.equals(e.key))
                        retries = 0;
                    else
                        e = e.next;
                }
                //尝试获取锁失败超过一定次数,直接lock()阻塞获取锁
                else if (++retries > MAX_SCAN_RETRIES) {
                    lock();
                    break;
                }
               //在循环尝试获取锁过程中,多线程下,如果这时其他线程有去改变当前链表上的值(头插 
                 法,所以不管改变那个值头结点会变),则 
                retries置为-1,重新遍历
                else if ((retries & 1) == 0 &&
                         (f = entryForHash(this, hash)) != first) {
                    e = first = f; // re-traverse if entry changed
                    retries = -1;
                }
            }
            return node;
        }

我们再看看扩容方法rehash():这个方法是在Segment<K,V>中的。

private void rehash(HashEntry<K,V> node) {
            HashEntry<K,V>[] oldTable = table;
            int oldCapacity = oldTable.length;
            int newCapacity = oldCapacity << 1;
            threshold = (int)(newCapacity * loadFactor);
            HashEntry<K,V>[] newTable =
                (HashEntry<K,V>[]) new HashEntry[newCapacity];
            int sizeMask = newCapacity - 1;
            for (int i = 0; i < oldCapacity ; i++) {
                HashEntry<K,V> e = oldTable[i];
                if (e != null) {
                    HashEntry<K,V> next = e.next;
                    int idx = e.hash & sizeMask;
                    if (next == null)   //  Single node on list
                        newTable[idx] = e;
                    else { // Reuse consecutive sequence at same slot
                        HashEntry<K,V> lastRun = e;
                        int lastIdx = idx;
                        //把最后几个连续节点(通过hash算出在新数组同一个位置)转移,只要转移一个 
                          就全部转移了,因为有指针链接
                        for (HashEntry<K,V> last = next;
                             last != null;
                             last = last.next) {
                            int k = last.hash & sizeMask;
                            if (k != lastIdx) {
                                lastIdx = k;
                                lastRun = last;
                            }
                        }
                        newTable[lastIdx] = lastRun;
                        // Clone remaining nodes
                        从头节点到lastRun节点遍历,转移到新数组
                        for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                            V v = p.value;
                            int h = p.hash;
                            int k = h & sizeMask;
                            HashEntry<K,V> n = newTable[k];
                            newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
                        }
                    }
                }
            }
            //转移完后,头插法把put的元素插进来
            int nodeIndex = node.hash & sizeMask; // add the new node
            node.setNext(newTable[nodeIndex]);
            newTable[nodeIndex] = node;
            table = newTable;
        }

ConcurrentHashMap扩容值针对SegMent内部扩容。而且在转移元素时与HashMap也有不一样:HashMap是一个一个元素转移,采用头插法方式,多线程下会造成死循环。而ConcurrentHashMap里分了两次,第一次循环把最后几个连续节点(通过hash算出在新数组同一个位置)转移,只要转移一个就全部转移了,因为有指针链接 。第二个循环从头节点到lastRun节点遍历,转移到新数组。

get(Object key)方法分析 

public V get(Object key) {
        Segment<K,V> s; // manually integrate access methods to reduce overhead
        HashEntry<K,V>[] tab;
        int h = hash(key);
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
        //通过UNSAFE方法拿第u个位置segment
        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
            (tab = s.table) != null) {
        //通过UNSAFE方法拿HashEntry[]数组里的HashEntry对象
            for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                     (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
                 e != null; e = e.next) {
                K k;
                if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                    return e.value;
            }
        }
        return null;
    }

                      

 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值