💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
[TOC] ## 初始化HashMap 新建一个HashMap(其实就是创建一个Entry数组) ``` Map<String, Object> map = new HashMap<String, Object>(); ``` 初始化HashMap(new HashMap())时,会将HashMap的初始容量设置为16,将负载因子设为0.75 ``` //默认容量大小 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 //默认负载因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); } ``` 然后继续初始化HashMap ``` public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); //HashMap最大容量是2的30次方 1073741824 if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; //初始化HashMap容量的阈值: 16;如果hashMap的容量大于16后,就会hashMap的扩容 threshold = initialCapacity; init(); } ``` 初始化完成后,向HashMap中设置值,即put方法 ## 向HashMap中设置值 put(K key, V value) ``` public V put(K key, V value) { if (table == EMPTY_TABLE) { /** * (1) 初始化table,new一个容量大小为16的Entry[]数组 * (2) 设置阈值threshold = 16*0.75(容量*负载因子) = 12 * (3) 如果有必要还会设置散列掩码值hashing mask value */ inflateTable(threshold); } // 如果key为空, put一个key为null的值 if (key == null) return putForNullKey(value); //计算key的hash值 int hash = hash(key); //计算Entry数组的下标 int i = indexFor(hash, table.length); //如果已经有了相同的key,则使用新值替换旧值,并返回旧值 // table[i]有可能是一个链表 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 value addEntry(hash, key, value, i); return null; } ``` 这里有个比较重要的地方 ``` for (Entry<K,V> e = table[i]; e != null; e = e.next) ``` 获取某个下标的HashMap值,可能获取到多个Entry数据,这个Entry数据是由链表组成,所有这里需要用到for循环,就是因为此处,会导致HashMap线程不安全(后面会具体说明原因); addEntry()源码,如果Entry数组的实际容量大小大于等于阈值(12),则需要对hashMap进行扩容(即新建一个Entry数组,其大小为现在数组的两倍大小),并把原Entry数组中的内容移到新Entry数组中,并重新计算key的hash值,作为新Entry数组的下标(参照下面resize()方法);如果Entry数组的实际容量大小小于阈值(12),则在原Entry数组中新添加一组数据。 ``` void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { //扩容Entry数组 resize(2 * table.length); hash = (null != key) ? hash(key) : 0; //新Entry数组的下标 bucketIndex = indexFor(hash, table.length); } //向Entry数组中添加一组数据 createEntry(hash, key, value, bucketIndex); } ``` 扩容Entry数组方法:resize() ``` void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } //新建一个Entry数组 Entry[] newTable = new Entry[newCapacity]; //将原数组的数据移到新数组中 transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); } ``` 将原数组移到新数组中,并重新计算每个key的hash值 ``` 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; } } } ``` 向Entry数组中新增一组数据 ``` void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; //新建一个Entry对象,并赋值,然后添加到table数组中 table[bucketIndex] = new Entry<>(hash, key, value, e); //添加完数据后,数组大小+1 size++; } ``` 向HashMap中添加key、value的流程基本就完成了。 ## 从HashMap中获取值 get(Object key) ``` public V get(Object key) { if (key == null) return getForNullKey(); Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); } ``` 获取key对应的Entry对象 ``` final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } int hash = (key == null) ? 0 : hash(key); for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } return null; } ``` 管中窥豹,其他方法不展开了。 ## 为什么HashMap不是线程安全的 (1)put操作线程不安全 在多线程的情况下,线程A和线程B同时对一个map的数组位置(table[1])进行put操作,两个线程同时得到table[1]链表的头结点,线程A执行完put操作后,将新值存储到了头结点的位置,而此时线程B才执行完put操作,则线程B存储的值会覆盖掉线程A存储的值,这样会导致线程A存储的值丢失。 (2)删除键值时线程不安全:原理同上 (3)当数组容量超过阈值时,同resize数组,从新建一个比原来容量大一倍的数组,此时当多个线程同时操作一个map的resize时,最后一个线程的resize会覆盖掉前面所有线程的resize操作,导致其他线程的数据都会丢失。