【HashMap底层实现原理】

待我称王封你为后i 2023-09-29 14:41 87阅读 0赞

目录

    1. 基于Arraylist集合方式实现
    1. 基于数组+链表方式实现(Jdk)
    1. HashMap底层是有序存放的吗?
    1. LinkedHashMap实现缓存淘汰框架
    1. HashMap如何降低Hash冲突概率
    1. HashMap源码解读
    • 6.1 modCount的作用
    • 6.2 HashMap7扩容产生死循环问题
    • 6.3 HashMap8扩容底层原理
    • 6.4 HashMap加载因子为什么是0.75而不是1或者0.5
    • 6.5 HashMap如何存放1万条key效率最高
    • 6.6 为什么JDK官方不承认Jdk7扩容死循环Bug问题
    1. ConcurrentHashMap源码解读
    • 7.1 JDK1.7ConcurrentHashMap源码解读
      • 7.1.1 简单实现ConcurrentHashMap
      • 7.1.2 核心参数分析
      • 7.1.3 核心Api
    • 7.2 JDK1.8ConcurrentHashMap源码解读
      • 7.2.1 源码解读
    1. Arraylist集合源码解读
    1. [HashMap源码注释](https://blog.csdn.net/qq\_30999361/article/details/124806516)

1. 基于Arraylist集合方式实现

  1. public class ArraylistHashMap<K, V> {
  2. private List<Entry<K, V>> entries = new ArrayList<Entry<K, V>>();
  3. class Entry<K, V> {
  4. K k;
  5. V v;
  6. Entry next;
  7. public Entry(K k, V v) {
  8. this.k = k;
  9. this.v = v;
  10. }
  11. }
  12. public void put(K k, V v) {
  13. entries.add(new Entry<>(k, v));
  14. }
  15. public V get(K k) {
  16. for (Entry<K, V> entry :
  17. entries) {
  18. if (entry.k.equals(k)) {
  19. return entry.v;
  20. }
  21. }
  22. return null;
  23. }
  24. public static void main(String[] args) {
  25. ArraylistHashMap<String, String> arraylistHashMap = new ArraylistHashMap<>();
  26. arraylistHashMap.put("demo", "123");
  27. arraylistHashMap.put("123", "demo");
  28. System.out.println(arraylistHashMap.get("123"));
  29. }
  30. }

根据key查询时间复杂度为o(n),效率非常低。

2. 基于数组+链表方式实现(Jdk)

在这里插入图片描述
HashMap1.7底层是如何实现的 基于数组+链表实现
同一个链表中存放的都是hashCode值可能相同,但是内容值却不同

  1. public class ExtHashMap<K, V> {
  2. private Entry[] objects = new Entry[10000];
  3. class Entry<K, V> {
  4. public K k;
  5. public V v;
  6. Entry next;
  7. public Entry(K k, V v) {
  8. this.k = k;
  9. this.v = v;
  10. }
  11. }
  12. public void put(K k, V v) {
  13. int index = k == null ? 0 : k.hashCode() % objects.length;
  14. Entry<K, V> oldEntry = objects[index];
  15. // 判断是否存在
  16. if (oldEntry == null) {
  17. objects[index] = new Entry<>(k, v);
  18. } else {
  19. // 发生hash碰撞则存放到链表后面
  20. oldEntry.next = new Entry<>(k, v);
  21. }
  22. }
  23. public V get(K k) {
  24. // if (k == null) {
  25. // for (Entry<K, V> oldEntry = objects[0]; oldEntry != null; oldEntry = oldEntry.next) {
  26. // if (oldEntry.k.equals(k)) {
  27. // return oldEntry.v;
  28. // }
  29. // }
  30. // }
  31. int index = k == null ? 0 : k.hashCode() % objects.length;
  32. for (Entry<K, V> oldEntry = objects[index]; oldEntry != null; oldEntry = oldEntry.next) {
  33. if (oldEntry.k == null || oldEntry.k.equals(k)) {
  34. return oldEntry.v;
  35. }
  36. }
  37. return null;
  38. }
  39. public static void main(String[] args) {
  40. ExtHashMap<Object, String> hashMap = new ExtHashMap<>();
  41. hashMap.put("a", "a");
  42. hashMap.put(97, "97");
  43. hashMap.put(null, "nulldemo");
  44. System.out.println(hashMap.get(97));
  45. }
  46. }

3. HashMap底层是有序存放的吗?

无序、散列存放
遍历的时候从数组0开始遍历每个链表,遍历结果存储顺序不保证一致。
如果需要根据存储顺序保存,可以使用LinkedHashMap底层是基于双向链表存放
LinkedHashMap基于双向链表实现
HashMap基本单向链表实现

4. LinkedHashMap实现缓存淘汰框架

LRU(最近少使用)缓存淘汰算法
LFU(最不经常使用算法)缓存淘汰算法
ARC(自适应缓存替换算法)缓存淘汰算法
FIFO(先进先出算法)缓存淘汰算法
MRU(最近最常使用算法)缓存淘汰算法

LinkedHashMap基于双向链表实现,可以分为插入或者访问顺序两种,采用双链表的形式保证有序
可以根据插入或者读取顺序

LinkedHashMap是HashMap的子类,但是内部还有一个双向链表维护键值对的顺序,每个键值对既位于哈希表中,也位于双向链表中。LinkedHashMap支持两种顺序插入顺序 、 访问顺序

插入顺序:先添加的在前面,后添加的在后面。修改操作不影响顺序
执行get/put操作后,其对应的键值对会移动到链表末尾,所以最末尾的是最近访问的,最开始的是最久没有被访问的,这就是访问顺序。
其中参数accessOrder就是用来指定是否按访问顺序,如果为true,就是访问顺序,false根据新增顺序

  1. public class LruCache<K, V> extends LinkedHashMap<K, V> {
  2. /**
  3. * 容量
  4. */
  5. private int capacity;
  6. public LruCache(int capacity) {
  7. super(capacity, 0.75f, true);
  8. this.capacity = capacity;
  9. }
  10. /**
  11. * 如果超过存储容量则清除第一个
  12. *
  13. * @param eldest
  14. * @return
  15. */
  16. @Override
  17. protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
  18. return size() > capacity;
  19. }
  20. public static void main(String[] args) {
  21. LruCache<String, String> lruCache = new LruCache<>(3);
  22. lruCache.put("a", "a");
  23. lruCache.put("b", "b");
  24. lruCache.put("c", "c");
  25. // ca e
  26. lruCache.get("a");
  27. //cae
  28. lruCache.put("e", "demo");
  29. lruCache.forEach((k, v) -> {
  30. System.out.println(k + "," + v);
  31. });
  32. }
  33. }

5. HashMap如何降低Hash冲突概率

  1. static final int hash(Object key) {
  2. int h;
  3. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  4. }
  5. ((p = tab[i = (n - 1) & hash])

1、保证不会发生数组越界
首先我们要知道的是,在HashMap,数组的长度按规定一定是2的幂。因此,数组的长度的二进制形式是:10000…000, 1后面有偶数个0。 那么,length - 1 的二进制形式就是01111.111, 0后面有偶数个1。最高位是0, 和hash值相“与”,结果值一定不会比数组的长度值大,因此也就不会发生数组越界。一个哈希值是8,二进制是1000,一个哈希值是9,二进制是1001。和1111“与”运算后,结果分别是1000和1001,它们被分配在了数组的不同位置,这样,哈希的分布非常均匀。

6. HashMap源码解读

6.1 modCount的作用

我们知道 java.util.HashMap 不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。这一策略在源码中的实现是通过 modCount 域,modCount 顾名思义就是修改次数,对HashMap 内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的 expectedModCount。在迭代过程中,判断 modCount 跟 expectedModCount 是否相等,如果不相等就表示已经有其他线程修改了 Map:

6.2 HashMap7扩容产生死循环问题

  1. void transfer(Entry[] newTable, boolean rehash) {
  2. int newCapacity = newTable.length;
  3. for (Entry<K,V> e : table) {
  4. while(null != e) {
  5. Entry<K,V> next = e.next;
  6. if (rehash) {
  7. e.hash = null == e.key ? 0 : hash(e.key);
  8. }
  9. int i = indexFor(e.hash, newCapacity);
  10. e.next = newTable[i];
  11. newTable[i] = e;
  12. e = next;
  13. }
  14. }
  15. }

6.3 HashMap8扩容底层原理

将原来的链表拆分两个链表存放; 低位还是存放原来index位置 高位存放index=j+原来长度
if ((e.hash & oldCap) == 0) { 由于oldCap原来的容量没有减去1 所以所有的hash&oldCap
为0或者1;

6.4 HashMap加载因子为什么是0.75而不是1或者0.5

产生背景:减少Hash冲突index的概率;
查询效率与空间问题;

简单推断的情况下,提前做扩容:

  1. 如果加载因子越大,空间利用率比较高,有可能冲突概率越大;
  2. 如果加载因子越小,有可能冲突概率比较小,空间利用率不高;
    空间和时间上平衡点:0.75
    统计学概率:泊松分布是统计学和概率学常见的离散概率分布

6.5 HashMap如何存放1万条key效率最高

参考阿里巴巴官方手册:
在这里插入图片描述

6.6 为什么JDK官方不承认Jdk7扩容死循环Bug问题

在这里插入图片描述
https://bugs.java.com/bugdatabase/view\_bug.do?bug\_id=6423457

7. ConcurrentHashMap源码解读

7.1 JDK1.7ConcurrentHashMap源码解读

使用传统HashTable保证线程问题,是采用synchronized锁将整个HashTable中的数组锁住,
在多个线程中只允许一个线程访问Put或者Get,效率非常低,但是能够保证线程安全问题。
在这里插入图片描述
Jdk官方不推荐在多线程的情况下使用HashTable或者HashMap,建议使用ConcurrentHashMap分段HashMap。
效率非常高。
在这里插入图片描述
ConcurrentHashMap将一个大的HashMap集合拆分成n多个不同的小的HashTable(Segment),默认的情况下是分成16个不同的
Segment。每个Segment中都有自己独立的HashEntry[] table;

7.1.1 简单实现ConcurrentHashMap

  1. public class MyConcurrentHashMap<K, V> {
  2. private Hashtable<K, V>[] segments;
  3. public MyConcurrentHashMap() {
  4. segments = new Hashtable[16];
  5. }
  6. public void put(K k, V v) {
  7. int segmentIndex = k.hashCode() & segments.length - 1;
  8. Hashtable hashtable = segments[segmentIndex];
  9. if (hashtable == null) {
  10. hashtable = new Hashtable<K, V>();
  11. }
  12. hashtable.put(k, v);
  13. segments[segmentIndex] = hashtable;
  14. }
  15. public V get(K k) {
  16. int segmentIndex = k.hashCode() & segments.length - 1;
  17. Hashtable<K, V> hashtable = segments[segmentIndex];
  18. if (hashtable != null) {
  19. return hashtable.get(k);
  20. }
  21. return null;
  22. }
  23. public static void main(String[] args) {
  24. MyConcurrentHashMap<String, String> concurrentHashMap = new MyConcurrentHashMap<>();
  25. // concurrentHashMap.put("a", "a");
  26. // concurrentHashMap.put("97", "97");
  27. for (int i = 0; i < 10; i++) {
  28. concurrentHashMap.put(i + "", i + "");
  29. }
  30. // System.out.println(concurrentHashMap.get("97"));
  31. for (int i = 0; i < 10; i++) {
  32. System.out.println(concurrentHashMap.get(i + ""));
  33. }
  34. }
  35. }

7.1.2 核心参数分析

  1. ##1.无参构造函数分析:
  2. initialCapacity ---16
  3. loadFactor HashEntry<K,V>[] table 加载因子0.75
  4. concurrencyLevel 并发级别 默认是16
  5. ##2. 并发级别是能够大于2的16次方
  6. if (concurrencyLevel > MAX_SEGMENTS)
  7. concurrencyLevel = MAX_SEGMENTS;
  8. ##3.sshift 左移位的次数 ssize 作用:记录segment数组大小
  9. int sshift = 0;
  10. int ssize = 1;
  11. while (ssize < concurrencyLevel) {
  12. ++sshift;
  13. ssize <<= 1;
  14. }
  15. ##4. segmentShift segmentMask:ssize - 1 做与运算的时候能够将key均匀存放;
  16. this.segmentShift = 32 - sshift;
  17. this.segmentMask = ssize - 1;
  18. ##5. 初始化Segment0 赋值为下标0的位置
  19. Segment<K,V> s0 =
  20. new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
  21. (HashEntry<K,V>[])new HashEntry[cap]);
  22. ##6.采用CAS修改复制给Segment数组
  23. UNSAFE.putOrderedObject(ss, SBASE, s0); // or
  24. Put方法底层的实现 简单分析
  25. Segment<K,V> s;
  26. if (value == null)
  27. throw new NullPointerException();
  28. ###计算key存放那个Segment数组下标位置;
  29. int hash = hash(key);
  30. int j = (hash >>> segmentShift28) & segmentMask15;
  31. ###使用cas 获取Segment[10]对象 如果没有获取到的情况下,则创建一个新的segment对象
  32. if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
  33. (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
  34. s = ensureSegment(j);
  35. ### 使用lock锁对put方法保证线程安全问题
  36. return s.put(key, hash, value, false);
  37. 0000 0000 00000 0000 0000 0000 0000 0011
  38. 0000 0000 00000 0000 0000 0000 0000 0011
  39. 深度分析:
  40. Segment<K,V> ensureSegment(int k)
  41. private Segment<K,V> ensureSegment(int k) {
  42. final Segment<K,V>[] ss = this.segments;
  43. long u = (k << SSHIFT) + SBASE; // raw offset
  44. Segment<K,V> seg;
  45. ### 使用UNSAFE强制从主内存中获取 Segment对象,如果没有获取到的情况=null
  46. if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
  47. ## 使用原型模式 将下标为0的Segment设定参数信息 赋值到新的Segment对象中
  48. Segment<K,V> proto = ss[0]; // use segment 0 as prototype
  49. int cap = proto.table.length;
  50. float lf = proto.loadFactor;
  51. int threshold = (int)(cap * lf);
  52. HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
  53. #### 使用UNSAFE强制从主内存中获取 Segment对象,如果没有获取到的情况=null
  54. if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
  55. == null) {
  56. // recheck
  57. ###创建一个新的Segment对象
  58. Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
  59. while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
  60. == null) {
  61. ###使用CAS做修改
  62. if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
  63. break;
  64. }
  65. }
  66. }
  67. return seg;
  68. }
  69. final V put(K key, int hash, V value, boolean onlyIfAbsent) {
  70. ###尝试获取锁,如果获取到的情况下则自旋
  71. HashEntry<K,V> node = tryLock() ? null :
  72. scanAndLockForPut(key, hash, value);
  73. V oldValue;
  74. try {
  75. HashEntry<K,V>[] tab = table;
  76. ###计算该key存放的index下标位置
  77. int index = (tab.length - 1) & hash;
  78. HashEntry<K,V> first = entryAt(tab, index);
  79. for (HashEntry<K,V> e = first;;) {
  80. if (e != null) {
  81. K k;
  82. ###查找链表如果链表中存在该key的则修改
  83. if ((k = e.key) == key ||
  84. (e.hash == hash && key.equals(k))) {
  85. oldValue = e.value;
  86. if (!onlyIfAbsent) {
  87. e.value = value;
  88. ++modCount;
  89. }
  90. break;
  91. }
  92. e = e.next;
  93. }
  94. else {
  95. if (node != null)
  96. node.setNext(first);
  97. else
  98. ###创建一个新的node结点 头插入法
  99. node = new HashEntry<K,V>(hash, key, value, first);
  100. int c = count + 1;
  101. ###如果达到阈值提前扩容
  102. if (c > threshold && tab.length < MAXIMUM_CAPACITY)
  103. rehash(node);
  104. else
  105. setEntryAt(tab, index, node);
  106. ++modCount;
  107. count = c;
  108. oldValue = null;
  109. break;
  110. }
  111. }
  112. } finally {
  113. ###释放锁
  114. unlock();
  115. }
  116. return oldValue;
  117. }

7.1.3 核心Api

GetObjectVolatile
此方法和上面的getObject功能类似,不过附加了’volatile’加载语义,也就是强制从主存中获取属性值。类似的方法有getIntVolatile、getDoubleVolatile等等。这个方法要求被使用的属性被volatile修饰,否则功能和getObject方法相同。

tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

7.2 JDK1.8ConcurrentHashMap源码解读

ConcurrentHashMap1.8的取出取消segment分段设计,采用对CAS + synchronized保证并发线程安全问题,将锁的粒度拆分到每个index
下标位置,实现的效率与ConcurrentHashMap1.7相同。
在这里插入图片描述

7.2.1 源码解读

sizeCtl :默认为0,用来控制table的初始化和扩容操作,具体应用在后续会体现出来。
-1 代表table正在初始化
N 表示有N-1个线程正在进行扩容操作
其余情况:
1、如果table未初始化,表示table需要初始化的大小。 0
2、如果table初始化完成,表示table的容量,默认是table大小的0.75倍,居然用这个公式算0.75(n - (n >>> 2))。

CounterCells 记录每个线程size++的次数

8. Arraylist集合源码解读

Arraylist底层是基于数组实现

  1. System.arraycopy(elementData, index+1, elementData, index, numMoved);

Object src : 原数组
int srcPos : 从元数据的起始位置开始
Object dest : 目标数组
int destPos : 目标数组的开始起始位置
int length : 要copy的数组的长度

  1. public class DemoArraylist<T> {
  2. /**
  3. * 存放数据元素
  4. */
  5. private Object[] elementData;
  6. /**
  7. * 记录存放的个数
  8. */
  9. private int size = 0;
  10. /**
  11. * 默认容量为10
  12. */
  13. private static final int DEFAULT_CAPACITY = 10;
  14. public void add(T t) {
  15. if (elementData == null) {
  16. elementData = new Object[10];
  17. }
  18. // 判断是否需要扩容
  19. if ((size + 1) - elementData.length > 0) {
  20. // 原来容量10
  21. int oldCapacity = elementData.length;
  22. // 新的容量 10+5;
  23. int newCapacity = oldCapacity + (oldCapacity >> 1);
  24. // 扩容
  25. elementData = Arrays.copyOf(elementData, newCapacity);
  26. }
  27. elementData[size++] = t;
  28. }
  29. public T get(int index) {
  30. return (T) elementData[index];
  31. }
  32. public boolean remove(T value) {
  33. for (int i = 0; i < size; i++) {
  34. T oldValue = (T) elementData[i];
  35. if (oldValue.equals(value)) {
  36. int numMoved = size - i - 1;
  37. if (numMoved > 0)
  38. //remove
  39. System.arraycopy(elementData, i + 1, elementData, i,
  40. numMoved);
  41. elementData[--size] = null;
  42. return true;
  43. }
  44. }
  45. return false;
  46. }
  47. public static void main(String[] args) {
  48. DemoArraylist<String> arraylist = new DemoArraylist<>();
  49. for (int i = 0; i < 100; i++) {
  50. arraylist.add("demo" + i);
  51. }
  52. arraylist.remove("demo2");
  53. // arraylist.add("demo11");
  54. for (int i = 0; i < arraylist.size; i++) {
  55. System.out.println(arraylist.get(i));
  56. }
  57. //
  58. // System.out.println(arraylist.get(0));
  59. }
  60. }

9. HashMap源码注释

发表评论

表情:
评论列表 (有 0 条评论,87人围观)

还没有评论,来说两句吧...

相关阅读

    相关 HashMap底层实现原理

    数据结构中有数组和链表这两个结构来存储数据。   数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入

    相关 HashMap底层实现原理

    数据结构中有数组和链表这两个结构来存储数据。 数组存储区间是连续的,占用内存严重,故空间复杂度很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删