Java 常用集合之HashMap和ConcurrentHashMap理解

JDK1.7 HashMap底层结构如下:

HashMap底层是基于 数组+链表 实现的。其中重要的两个参数

  • 容量
  • 负载因子

默认的初始容量大小是16,负载因子是0.75,当 HashMap 的 size > 16*0.75 时就会发生扩容,扩容这个过程涉及到rehash、复制数据等操作,很耗性能。我们可以通过预估容量大小来减少扩容次数

负载因子:

public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
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;
    threshold = initialCapacity;
    init();
}

put 方法

首先会将传入的 key 的 hash 运算计算出 hashcode,然后根据数组长度取模计算出数组中的 index 下标。由于在计算中位运算比取模运算效率高的多,所以 HashMap 规定数组的长度为 2^n,这样用 2^n - 1,做位运算与取模效果一致,并且效率高,由于数组的长度有限,不同的 key 通过运算得到的 index 相同,这种情况下可以利用链表来解决,HashMap 会在 table[index] 处形成链表 ,采用头插法将数据插入到链表中。

get 方法

将传入的 key 计算出 index ,如果该位置上是一个链表就需要遍历整个链表,通过 key.equal(key) 来找到对应的元素

public V get(Object key) {
    if (key == null)
        return getForNullKey();
    Entry<K,V> entry = getEntry(key);
    return null == entry ? null : entry.getValue();
}
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;
}

注: 当Hash冲突严重时,在桶上形成的链表就会越来越长,这样查询时的效率就会越来越低,时间复杂度为 O(n)

 

JDK1.8后的优化

1.8之后的 ConcurrentHashMap 抛弃了原有的Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性,底层实现也改为了 数组+链表+红黑树,当链表节点较多时(大于8)会转为红黑树,红黑树的查找时,会反复用到以下两条规则:

1) 如果目标节点的hash值小于 p 节点的 hash 值,则向 p 节点的左边遍历,否则向 p 节点的右边遍历

2)如果目标节点的 key 值小于 p 节点的 key 值,则向 p 节点的左边遍历;否则向 p 节点的右边遍历。这两条规则是利用了红黑树的特性(左节点<根结点<右节点)

修改为红黑树之后查询的时间复杂度为O(logn)

HashMap 的遍历代码推荐

terator<Map.Entry<String, Integer>> entryIterator = map.entrySet().iterator();
   while (entryIterator.hasNext()) {
        Map.Entry<String, Integer> next = entryIterator.next();
        System.out.println("key=" + next.getKey() + " value=" + next.getValue());
    }

ConcurrentHashMap 解析:

如图所示,ConcurrentHashMap 由Segment数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表

ConcurrentHashMap 采用了分段锁技术,其中Segment继承于ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上ConcurrentHashMap 支持 CurrencyLevel(Segment数组数量)的线程并发,每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment

get 方法

ConcurrentHashMap 的 get 方法,不需要任何加锁的操作,只需要将key 通过 hash 之后定位到具体的 Segment,再通过一次 Hash 定位到具体的元素上,由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以保证了每次获取都是最新值

put 方法

内部的HashEntry 类:

 static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;

        HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }

虽然 HashEntity 中的 value 是用 volatile 关键词修饰,但是并不能保证并发的原子性,所以put操作时仍然需要加锁处理,首先通过 Key 的 Hash 定位到具体的Sement,在 put 之前会进行一次扩容校验,这里比 HashMap 要好的一点是:ConcurrentHashMap 扩容操作是在操作之前决定的,而HashMap是数据插入后,再检查是否需要扩容,后者直接导致扩容后没有后续操作,浪费了一次扩容(扩容很消耗性能);

size 方法

每个 Segment 都有一个volatile 修饰的全局变量 count,求整个 ConcurrentHashMap 的 size 时很明显就是将所有的 count 累加即可,但是 volatile 修饰的变量却不能保证多线程下的原子性,所以直接累加很容易出现并发问题,但如果每次调用 size 方法将其余的修改操作加锁,效率也不是很高,所以做法为先尝试两次 将 count 累加,如果容器的 count 发生了变化 再加锁来统计 size,而 ConcurrentHashMap 是如何检测统计时的发生了变化?每个 Segment 都有一个 modCount 变量,每次进行一次 put remove 等操作,modeCount 将会 +1,只要modCount 发生了变化就会认为 容器的大小也发生了变化

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值