HashMap与ConcurrentHashMap

首先我们来说一下HashMap的数据结构,在Java编程语言中,最基本的结构有两种,一个是数组,一个是指针,也就是我们平时说的引用,HashMap就是基于这两个数据结构实现的
在这里插入图片描述
我们从这个图可以看出来,HashMap的底层就是一个数组结构,而数组中的明细呢,是一个链表,当我们初始化一个HashMap的时候,就会初始化一个数组出来,HashMap有两个参数影响它的性能,它们分别是初始容量和加载因子,我们来看一下源代码

 /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

这里是它的初始容量,默认值是16;

/**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

默认加载因子为0.75

当哈希表中的数量超过的加载因子和我们的当前容量的乘积,比如默认的16*0.75=12的时候,将会调用resize()方法进行扩容,将容量翻倍.

当然初始容量和加载因子在初始化的时候是可以指定的,我们来看看HashMap的构造函数
在这里插入图片描述
HashMap一共提供了4个构造函数,默认是什么值都不传的,这时这个HashMap的容量和加载因子都使用默认提供的,HashMap(int)指定初始容量;

public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

HashMap(int,float)可以指定初始容量和加载因子(这里就不上源码了)

HashMap的寻址方式

对于一个新插入的数据,或者我们需要读取的数据,HashMap需要参照一个计算规则,来计算出hash值,并对我们的数组长度取模,结果就是数组中参照的index,在计算机中取模的代价远远大于位运算的代价,因此HashMap要求数组的长度必须是2^n,此时它将替代hash值对2的n-1次方进行与运算,它的结果与我们的取模操作是相同的,HashMap并不要求我们用户在指定HashMap容量时必须要传入2的n次方的整数,而是在初始化时根据传入的容量值计算出一个满足2的元素,我们来看一下他的代码.

public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

它真正在触发容量的时候是调用一个tableSizeFor的方法
tableSizeFor传入的是一个用户输入容量值,通过一些位运算算出一个合适的容量值.

static final int tableSizeFor(int cap) {
        int n = cap - 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;
    }

众所周知HashMap是线程不安全的,而HashMap的线程不安全主要体现在resize()方法,可能会出现死循环,当HashMap的size超过加载因子与初始容量的乘积后,就会对HashMap进行扩容,具体方法是需要创建一个长度为原来两倍的数组,保证新的容量仍然为2的n次方,从而保证上述取址的方式仍然适用,同时需要将原来的数组全部重新插入,这个工作我们称为rehash,这个方法并不保证线程安全.而且在多线程并发调用时可能会出现死循环.
现在我们先来看一下单线程下的rehash:
在这里插入图片描述
接下来使用这个新的数组以及它下面的链表代替原来的数组来进行相关的存储,这样就完成了一次rehash的操作,在单线程下,这个rehash是没有问题的.
接下来我们重点看一下多线程并发下的rehash

在这里插入图片描述

我们重点看一下多线程并发下的rehash,我们假设有两个线程,同时执行put的操作,并同时触发了rehash的操作,我们假设上面的是线程一下面的是线程二,同时我们假设线程一当前执行到了申请两倍容量的数组,然后准备处理第一个元素5,跟他的下一个指针指向的元素9,因为线程调度所分配的时间片用完而他临时暂停了,所以这时他临时暂停了这时候呢,线程二尽快开始准备操作,并且执行完了操作,具体就是我们图里展示的,所以呢,我们要做的是当前线程一案准备处理五这个元素,并且指针指向的是9,而线程二呢,他已经安全做完了,rehash的操作,接下来呢线程一被唤醒了,接着继续操作,执行刚才循环的剩余部分.
在这里插入图片描述
首先处理5这个元素的时候呢,他将5放在我们线程1申请的这个数组的索引1位置的链表的首部,5后面是个空值代表链表后面没有值了,然后呢,5后面的元素是我们刚才设的9,9是等待处理的 这是第一步
在这里插入图片描述
第二步呢,我们处理完5之后继续处理9,处理9之后呢,相当于在线程一申请了这个新的数组,在5的前面放入9,参照上图中的箭头方向这样过来的,索引1这个链表的第一个是9,然后是5,最后是后面的空值,这个时候我们需要注意的是,9这个元素我们处理完了,而我们线程二在处理的时候9后面已经新增了元素,需要处理,接来下处理5的时候呢(如下图),他会再次这个5放到我们这个线程一里面形成索引1的链表的首部
在这里插入图片描述
这个时候呢,就会出现一个循环链表
我们需要注意的是11这个元素是无法加入线程1这个新的数组里面了,这个时候在下一次访问的时候,一旦访问到这个链表,就会出现死循环,这就是HashMap在多线程环境下扩容容易出现的死循环…

接下来我们在使用迭代器的过程中,如果HashMap被修改了,那么就会抛出ConcurrentModificationException就是我们所说的Fast-fail策略,在多线程条件下我们可使用Collections.synchronizedMap方法构造出一个同步Map,或者直接使用线程安全的ConcurrentHashMap。

接下来我们来重点介绍ConcurrentHashMap
在这里插入图片描述Java7中的ConcurrentHashMap它的底层结构仍然是数组和链表,和HashMap不同的是ConcurrentHashMap的内层不是一个大的数组,而是一个Sement数组,每个Sement包含和HashMap数据结构差不多的链表数组,整体结构如上图所示.在我们读取某key的时候,它先取出那个数的hash值,并将hash的后n位对我们的segment的个数取模,从而得到该key属于那个segment,接着就像操作HashMap一样操作这个segment,为了保证不同的值均匀的分布到不同的segment里面,它计算hash值也做了专门的优化,这个segment他是继承自我们JUC里的ReentrantLock,所以我们可以很容易的对每个segment进行上锁.
需要注意的是jdk7以及之前的版本中ConcurrentHashMap它是基于我们这里说的分段锁来进行处理的

接下来我们来介绍下ConcurrentHashMap和HashMap的不同点:

  • ConcurrentHashMap是线程安全的,HashMap是非线程安全的
  • HashMap允许key value为空 ,而ConcurrentHashMap是不允许的
  • HashMap不允许通过Iterator遍历的同时通过HashMap来修改,而ConcurrentHashMap是允许的 并且这个更新对后续的遍历是可见的

Java7对并行访问引入的segment结构,实现了分段锁,理论上并发数与segment的个数是相等的.

Java8为了进一步提升并发性,他废弃了分段锁方案,并且直接使用了一个大的数组,同时为了提高哈希碰撞下的寻址做了性能优化,Java8在链表长度超过一定的值,默认值是8,这里的列表转换成了红黑树
在这里插入图片描述
它的查找的时间复杂度从链表的O(n)转换成了O(logn),这是一个性能的极大提升,Java8里的ConcurrentHashMap同样是通过key的hash值与数组长度取模确定该key在数组中的索引

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值