美文网首页
聊聊HashMap

聊聊HashMap

作者: jqdywolf | 来源:发表于2018-03-14 11:02 被阅读0次

    HashMap

    jdk1.7实现版本

    数组+链表

    • 引入
      数组+链表。很简单,初始化一个数组,解决冲突的办法就是开链法:将hashCode相同的元素放入链表中。


      image.png
    • 非线程安全
      hashmap是非线程安全的,即hashmap的实现中不涉及同步锁的概念。所以多个线程put或一个线程put一个线程get都会引起同步问题。
    • resize操作
      • 装载因子:hashmap总元素个数/数组长度
      • 在put方法中,如果发现新增元素后,hashmap的装载因子过高(默认是0.75),就会自动触发resize操作。
      • resize操作的步骤:申请两倍大小的数组,将原数组中的链表一次转移到新的数组中。具体操作是修改对象引用,并不是完全new每个链表的每个结点。
      • 上述的转移操作可能会引发死循环问题:多个线程同时put,同时触发resize操作,在转移链表的过程中,引起了引用的回路,造成死循环。可能会把CPU打到100%。
    jdk1.8实现版本

    数组+链表+红黑树

    • 并不解决线程安全问题,而是提升了get方法的效率。
    • 当一个链表的个数打到某个阈值(默认8)时,就会使用红黑树来代替链表。显然get效率从之前的O(n)->O(logn)。

    ConcurrentHashMap

    上面说hashmap并不是线程安全的,ConcurrentHashMap就是用来解决线程安全的。

    jdk1.7实现版本

    数组+segment+链表
    同步:segment+ReentrantLock

    • 数据结构


      image.png

    如图所示,map由1.7中的两级被分为了三级。首先hashmap中存的是segment数组。每个segment数组中存放一个实体数组,每个数组中是一个链表结构。

    • 如何解决线程安全问题?
      • 每个segment对象继承了ReentrantLock,即对分段进行加锁管理。
      • put操作
        先获取锁:自旋的retryLock,一段时间后再进行阻塞的lock。
        然后再进行写入操作
      • get操作
        并不加锁。get操作使用的共享变量都被声明为valotile,保证了线程可见性,所以可以直接读取。除了需要链表顺序查找,这个设计还是很棒的。
    • 缺点
      • size()得到的结果可能不是特别准
        jdk7对于ConcurrentHashMap size的内部实现:依次对每个segment进行size操作,然后将每个segment的size相加。整个map连续进行两次这样的操作,如果两次的结果相同则返回。否则锁住整张表。
      • 我们发现其实锁的粒度为segment,这个粒度还是有点粗,即修改同一个segment下的两个不同的元素也会被阻塞。如果可以是每个数组元素就很好了。
    jdk1.8实现版本

    数组+链表+红黑树
    同步:CAS+synchronized

    目的就是为了提高了1.7效率。
    如何提高?

    • 数据结构
      和jdk1.8中的hashmap一样,使用红黑树代替链表。
      取消了segment三层结构,使用和hashmap一样的两层结构。
    • 锁的粒度
      上面我们知道1.7中锁的粒度为segment,而在1.8中取消了segment。锁的粒度变成了每个数组的元素(使用synchronized)。这样引发了一个问题,那么跨数组元素的操作怎么办?比如size操作。
      • 解决办法:将map的size变量设为原子性变量,比如AtomicInteger变量。每次put或remove维护这个变量即可。
    • 具体操作
      • put操作。先使用CAS进行put,如果失败则使用synchronized加锁。
      • get操作。和1.7一样,使用valotile保证效率。
      • size操作。每次put或remove维护一个原子变量即可。

    相关文章

      网友评论

          本文标题:聊聊HashMap

          本文链接:https://www.haomeiwen.com/subject/ipbzfftx.html