循序渐进探索HashMap与ConcurrentHashMap

本文通过探索HashMap的使用和存在的问题,循序渐进的探索和引入各种原理和编程思路。主要内容包括:

  • 探索HashMap

    • 什么是HashMap
    • HashMap如何扩容
    • 为什么HashMap的初始化容量默认为16
    • 负载因子为什么是0.75
    • hash方法的具体实现
    • LinkedHashMap实现LRU缓存
    • HashMap存在的问题及解决办法
  • 探索ConcurrentHashMap

    • 分段锁
    • CAS+Synchronized
    • 初始化构造原理

探索HashMap

  • 1、【什么是HashMap】

    • 数据结构:HashMap是一个数组加链表的数据结构(链地址法)。
    • 使用:加入数据首先经过hash,放入计算出的位置,如果该位置有值,即发生hash冲突,一般hash冲突有四种处理方式:
      • 链地址法:该地址存放的是一个链表,直接在链表尾端加入数据即可
      • 开放定址法:顺延下一个不冲突的地址。
      • rehash:使用另一种hash算法
      • 建立公共溢出区:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。
  • 2、【HashMap如何扩容】

    • HashMap里面有数组,数组上挂的是链表,数组有个设定值,链表也有个扩容长度,当链表长度大于8,进一步判断数组长度,
    • 数组长度小于设定值MIN_TREEIFY_CAPACITY(64),只是对数组进行扩容,把已存数据进行再次hash处理。
      • 每次扩容,数组长度增加一倍。
      • 如果预先知道要存储1000个数,设置多大的 HashMap比较合理:1000<0.75x,求出x是1334,new HashMap(1334),自动会把存储大小改成大于1334的最小2的次方:2048。
    • 数组长度超过64时,将链表转换成红黑树。
    • 参考:HashMap中的链表什么时候转化为红黑树
  • 2.1、【为什么HashMap的初始化容量默认为16】

    • 为什么可以使用位运算(&)来实现取模运算(%)呢?这实现的原理如下:
      • X % 2^n = X & (2^n – 1)
    • 选16是经验值
    • 如何找到比传入值大的最小2的次方:数学问题,不会。
// 找到第一个大于等于initialCapacity的2的平方的数
int capacity = 1; 
while (capacity < initialCapacity)
       capacity <<= 1;
  • 2.2、【负载因子为什么是0.75】

    • 因为综合考虑时间复杂度和空间复杂度,折中选择0.75.
  • 2.3、【hash方法的具体实现】

static int indexFor(int h, int length) {
    
    
     return h & (length - 1);
}
  • 为什么是length - 1

    • 因为length一般是2的次方,会导致奇数位的数据hash不到。
  • 3、【LinkedHashMap实现LRU缓存】

    • LinkedHashMap支持两种顺序插入顺序 、 访问顺序
      • 插入顺序:先添加的在前面,后添加的在后面。修改操作不影响顺序
      • 访问顺序:所谓访问指的是get/put操作,对一个键执行get/put操作后,其对应的键值对会移动到链表末尾,所以最末尾的是最近访问的,最开始的是最久没有被访问的,这就是访问顺序。
      • 参考: LinkedHashMap基本用法&使用实现简单缓存
  • 4、【多线程情况下HashMap存在的问题及解决办法】

    • 4.1、 存在的问题:多线程环境下,
    • 4.2、解决办法
      • 使用Hashtable,使用synchronized来保证线程安全。
      • 但是使用HashTable会引起效率问题,同一把锁效率太低。
      • 于是引入ConcurrentHashMap,在jdk1.8之前使用分段锁,不同的segment是不同的锁,但是即使这样仍然会减小效率。
      • 在jdk1.8之后,ConcurrentHashMap引入了无锁模式。

探究ConcurrentHashMap

  • 分段锁(1.8以前)
    • ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock
  • CAS+Synchronized(1.8)
  • 初始化构造原理
//返回一个大于输入参数且最小的为2的n次幂的数。
private static final int tableSizeFor(int c) {
    
    
        int n = c - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
//tableSizeFor(int c)的原理:
//将c最高位以下通过|=运算全部变成1,最后返回的时候,返回n+1;

猜你喜欢

转载自blog.csdn.net/ljfirst/article/details/107813985