Map_keyVal_mode

Posted AmoryWang_JavaSunny

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Map_keyVal_mode相关的知识,希望对你有一定的参考价值。

首先为什么取这个名字呢,因为我主要是想整合几篇关于映射集合的内容,从上往下,有浅至深的讲述这个知识点

First one : Map 没有继承Collection 还是不是集合?

Map interface 是 属于集合,但此集合非彼集合,因为我所说的是,Map是一种可以储存一系列有相同定义的值,那就是集合,也就是定义集合的关键,Map具备,只不过,Map 更侧重于值之间的映射,这也是为什么他要单独出来自创天下的原因,区别于Collection下继承类储存值得方式。


Second one :  Map 中常见的几个Map,及其区别?

Map 中我们常常见到主要是 HashMap , Hashtable,  ConcurrentHashMap, synchronizedMap

这几个我在这里面主要写HashMap 在java中的实现,因为他的实现思想就是其他的Map实现思想精髓,其他的只是在他的基础上添加了一些自己独有的东西,来优化,提供不同业务下的多样选择性

Second one_1 HashMap 实现 +源码

用过HashMap<k, v> 都知道,浅层次的理解就是,一个k对应一个v,但是除了可以有这样一个可以储存映射关系的好处外,从结构上我们也可以挖掘一些他的优势:

 

 1     /**
 2      * The default initial capacity - MUST be a power of two.
 3      */
 4     static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认16
 5 
 6     /**
 7      * The maximum capacity, used if a higher value is implicitly specified
 8      * by either of the constructors with arguments.
 9      * MUST be a power of two <= 1<<30.
10      */
11     static final int MAXIMUM_CAPACITY = 1 << 30;
12 
13     /**
14      * The load factor used when none specified in constructor.
15      */
16     static final float DEFAULT_LOAD_FACTOR = 0.75f; // 实际的储存空间能力
17 
18     /**
19      * The table, resized as necessary. Length MUST Always be a power of two.
20      */
21     transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
22 
23     /**
24      * The number of key-value mappings contained in this map.
25      */
26     transient int size;
27 
28     /**
29      * The next size value at which to resize (capacity * load factor).
30      * @serial
31      */
32     // If table == EMPTY_TABLE then this is the initial capacity at which the
33     // table will be created when inflated.
34     int threshold;
35 
36     /**
37      * The load factor for the hash table.
38      *
39      * @serial
40      */
41     final float loadFactor;
42 
43     /**
44      * The number of times this HashMap has been structurally modified
45      * Structural modifications are those that change the number of mappings in
46      * the HashMap or otherwise modify its internal structure (e.g.,
47      * rehash).  This field is used to make iterators on Collection-views of
48      * the HashMap fail-fast.  (See ConcurrentModificationException).
49      */
50     transient int modCount;

 

 

 解释一下  : 

transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;  这是一个链表数组,也就是说,其实Map的结构就是一个数组,只不过这个数组的每一个值有点特殊,是一个链表形式的。

附图:

 

所以这个结构也是显而易见的, 再来看看其他几个属性 : (看看大神都是用的位运算来优化速度的。。。牛逼)

DEFAULT_INITIAL_CAPACITY = 1 << 4  --> 默认entry初始的个数就是16, 且一定要是2的次方数(这个原因后面讲),下面简称cap
static final float DEFAULT_LOAD_FACTOR = 0.75f;  -->实际的存放数的能力,这里举个例子 : cap = 16, 这里值是0.75, 那么可以存放的size - cap * 0.75 = 12,当大于12, 那map就会执行resize(),进行2倍的扩容
transient int modCount; // 来监测fail-fast的,这个跟List里面的增删的modConut有异曲同工之处,一旦不一致,则抛出异常

然后来讲一讲map映射原理 : 为什么大多数情况下可以做到一一对应起来:

还是一样, 源码上起来 : 

 1     public V put(K key, V value) {
 2         if (table == EMPTY_TABLE) {
 3             inflateTable(threshold);
 4         }
 5         if (key == null)
 6             return putForNullKey(value); // 允许有空值,并且将key=null的value放入table[0]中
 7         int hash = hash(key);
 8         int i = indexFor(hash, table.length);
 9         for (Entry<K,V> e = table[i]; e != null; e = e.next) {
10             Object k;
11             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
12                 V oldValue = e.value;
13                 e.value = value;
14                 e.recordAccess(this);
15                 return oldValue;
16             }
17         }
18 
19         modCount++;
20         addEntry(hash, key, value, i);
21         return null;
22     }

 

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

        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); // 再次hash了一遍
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

    /**
     * Returns index for hash code h.
     */
    static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

 

首先算出key 的hash值,通过算hash的方式,而且源码中,除了正常的hashCode以外还进行了位运算算了hash值,至于原理是什么我也不太清楚,但目的应该就是尽量减小碰撞,(碰撞就是,不同的对象算出来的hashCode一样,那就必须添加链表了),其次indexFor就是通过算出来的hash来确定在table数组中的下标位置,还是位运算,但是这个位运算其实很有意思,就是一种巧妙地取余操作, eg : h = 14, length = 16, length-1 = 15,那 进行h & (length-1) [假设8位]--> 0000 1110 & 0000 1111 = 0000 1110, 还是14 ,或者 h = 18 --> 0001 0010 & 0000 1111 = 0000 0010  为 2 ,也是余数,很神奇对吧, 其实原理就是,16 是 2^n , n = 4, 也就是二进制中会有 4个0,而它再减一,则会得到离它最近的二进制中1最多的情况,也就是15会有4个1,这是一种很特殊的情况,因为对于与操作来说,只有都是1的情况才会是1, 则这极大程度上把决定结果数字的权利交给其余的数字, 这也是这个式子的奇妙之处(清楚了是不是感觉就很简单),所以这也解释了上面的cap为什么一定会是2的次方的原因

这里补充一点 : hash相同的,但是不一定equals,equals 就是一定hash一样

 

差不多原理就是这样的,现在我们再来看看HashMap经常我们会说到它的线程不安全性,这是为什么?

当然单线程条件下,对于数据来说是没有任何影响,都是JVM按照Class文件中的一步一步来嘛

但是再多线程的条件下,就要考虑一下操作是不是符合ACID原则了,A 原子性, C一致性 I不可分割  D隔离性

所以一般对于HashMap来说两种方式来考虑线程不安全:

第一种 : 两个线程同时进行put 操作, 前面已经给出了put的源码 ,可以看见关键性的一句 : 

 addEntry(hash, key, value, i);

这句没有进行任何的上锁操作, 所以情况是两个线程,刚刚好同时有hash算出来一致的值,那很有可能会出现遗漏的情况,要不是显线程一的那个value没有插入,要么可能就是线程2的。

第二种 : 主要是发生在size需要扩容的情况下,会产生死循环的情况:(主要源码)

 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];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

    /**
     * Transfers all entries from current table to newTable.
     */
    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;
            }
        }
    }

 

在resize()操作中,有一个过渡的函数transfer ;其实会出现死循环的主要原因就在这里面,这个函数,主要是做对entry进行新的indexFor的修改索引的值,然后找到了新的容量下的table的相应的index之后,现在要移位置了,这里是关键,他是用的头节点插入链接法来链接移位的,下图所示

 

 以前的3->7 现在变成了7->3 

所以这样的操作很容易在多线程的情况下,出现环形连接,一个线程由3->7然后,挂起, 另外一个线程已经做完transfer操作,由7->3,这样就会形成环形连接,之后,永远get下一个值得情况,就出现异常

 

 

 

 

 

 

 

 所以有这样的不足,就会有弥补的方式,有一个Hashtable和synchronizedMap 我觉得他们俩的效率都一样不是很高,因为他们都是对整张table进行的加锁 ,Hashtable的加锁对所以方法加锁 ,synchronizedMap是对Map中的一个对象进行加锁,简单展示一下:

 final Object      mutex;        // Object on which to synchronize
public int size() {
            synchronized (mutex) {return m.size();}
        }
        public boolean isEmpty() {
            synchronized (mutex) {return m.isEmpty();}
        }
        public boolean containsKey(Object key) {
            synchronized (mutex) {return m.containsKey(key);}
        }
        public boolean containsValue(Object value) {
            synchronized (mutex) {return m.containsValue(value);}
        }
        public V get(Object key) {
            synchronized (mutex) {return m.get(key);}
        }

        public V put(K key, V value) {
            synchronized (mutex) {return m.put(key, value);}
        }
        public V remove(Object key) {
            synchronized (mutex) {return m.remove(key);}
        }
 

 

所以这种效率不高,相当于同步操作了

因而 , 在current并发包中一个奇妙的优化currentHashMap就诞生了,详解可以看我的另外一篇文章,因为讲起来还是有点多,这篇已经写得挺多的了。。。

 

以上是关于Map_keyVal_mode的主要内容,如果未能解决你的问题,请参考以下文章

VSCode自定义代码片段——CSS选择器

谷歌浏览器调试jsp 引入代码片段,如何调试代码片段中的js

片段和活动之间的核心区别是啥?哪些代码可以写成片段?

VSCode自定义代码片段——.vue文件的模板

VSCode自定义代码片段6——CSS选择器

VSCode自定义代码片段——声明函数