一篇文章解决HashMap所有问题

HashMap简介
众所周知HashMap底层是基于数组+链表 组成的,它是一个散列表,它存储的内容是键值对(key-value)映射。HashMap继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。HashMap的实现不是同步的,这意味着它不是线程安全的。他的key、value都可以为null。此外,HashMap中的映射不是有序的。
HashMap的组成
HashMap共有4个构造函数,如下:
// 默认构造函数。
HashMap()
//指定“容量大小”的构造函数。
HashMap(int capacity)
//指定“容量大小”和“加载因子”的构造函数
HashMap(int capacity,float loadFactor)
// 包含“子Map”的构造函数
HashMap(Map<? extends K,? extends V> map)
HashMap的API:
clear();clone();containsKey(Ojbect key);containsValue(Object value);entrySet();get(Object key);isEmpty();keySet();put(K key,V value);
putAll(Map<? extends K,? entends V> map);remove(Object key);size();values();
HashMap的继承关系:
java.lang.Object
↳ java.util.AbstractMap<K, V>
↳ java.util.HashMap<K, V>

public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable { }
HashMap 和 Map 关系如下图:
HashMap和Map关系图
从途中可以看出:

  1. HashMap继承于AbstractMap类,实现了Map接口。Map是“key-value键值对”接口,AbtractMap实现了“键值对”的通用函数接口。

  2. HashMap是通过“拉链法”实现的哈希表。它包含几个重要的成员变量:table,size,threshold,loadFactor,modCount;table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。哈希表的“key-value键值对”都是存储在Entry数组中的。size是HashMap的大小,它是HashMap保存的键值对的数量。threshold是HashMap的阈值,用于判断是否需要调整HashMap的容量。threshold的值=“容量*加载因子”,当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍。loadFactor就是加载因子。 modCount是用来实现fail-fast机制的。
    源码解析:
    中所周知HashMap底层基于 数组 + 链表 组成的,不过在jdk1.7和1.8中具体实现稍有不同。
    jdk1.7 中的数据结构:
    HashMap数据结构图
    HashMap存储实现:put(key,value)
    首先我们先看源码
    public V put(K key, V value) {
    //当key为null,调用putForNullKey方法,保存null与table第一个位置中,这是HashMap允许为null的原因
    if (key == null)
    return putForNullKey(value);
    //计算key的hash值
    int hash = hash(key.hashCode()); ------(1)
    //计算key hash 值在 table 数组中的位置
    int i = indexFor(hash, table.length); ------(2)
    //从i出开始迭代 e,找到 key 保存的位置
    for (Entry<K, V> e = table[i]; e != null; e = e.next) {
    Object k;
    //判断该条链上是否有hash值相同的(key相同)
    //若存在相同,则直接覆盖value,返回旧value
    if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
    V oldValue = e.value; //旧值 = 新值
    e.value = value;
    e.recordAccess(this);
    return oldValue; //返回旧值
    }
    }
    //修改次数增加1
    modCount++;
    //将key、value添加至i位置处
    addEntry(hash, key, value, i);
    return null;
    }
    通过源码我们可以清晰看到HashMap保存数据的过程为:首先判断key是否为null,若为null,则直接调用putForNullKey方法。若不为空则先计算key的hash值,然后根据hash值搜索在table数组中的索引位置,如果table数组在该位置处有元素,则通过比较是否存在相同的key,若存在则覆盖原来key的value,否则将该元素保存在链头(最先保存的元素放在链尾)。若table在该处没有元素,则直接保存。这个过程看似比较简单,其实深有内幕。有如下几点:

    1、 先看迭代处。此处迭代原因就是为了防止存在相同的key值,若发现两个hash值(key)相同时,HashMap的处理方式是用新value替换旧value,这里并没有处理key,这就解释了HashMap中没有两个相同的key。

    2、 在看(1)、(2)处。这里是HashMap的精华所在。首先是hash方法,该方法为一个纯粹的数学计算,就是计算h的hash值。
    static int hash(int h) {
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
    }
    我们知道对于HashMap的table而言,数据分布需要均匀(最好每项都只有一个元素,这样就可以直接找到),不能太紧也不能太松,太紧会导致查询速度慢,太松则浪费空间。计算hash值后,怎么才能保证table元素分布均与呢?我们会想到取模,但是由于取模的消耗较大,HashMap是这样处理的:调用indexFor方法。

static int indexFor(int h, int length) {
return h & (length-1);
}
HashMap的底层数组长度总是2的n次方,在构造函数中存在:capacity <<= 1;这样做总是能够保证HashMap的底层数组长度为2的n次方。当length为2的n次方时,h&(length - 1)就相当于对length取模,而且速度比直接取模快得多,这是HashMap在速度上的一个优化。至于为什么是2的n次方下面解释。

  我们回到indexFor方法,该方法仅有一条语句:h&(length - 1),这句话除了上面的取模运算外还有一个非常重要的责任:均匀分布table数据和充分利用空间。
  这里我们假设length为16(2^n)和15,h为5、6、7。
  这里我们再来复习put的流程:当我们想一个HashMap中添加一对key-value时,系统首先会计算key的hash值,然后根据hash值确认在table中存储的位置。若该位置没有元素,则直接插入。否则迭代该处元素链表并依此比较其key的hash值。如果两个hash值相等且key值相等(e.hash == hash && ((k = e.key) == key || key.equals(k))),则用新的Entry的value覆盖原来节点的value。如果两个hash值相等但key值不等 ,则将该节点插入该链表的链头。具体的实现过程见addEntry方法,如下:

void addEntry(int hash, K key, V value, int bucketIndex) {
//获取bucketIndex处的Entry
Entry<K, V> e = table[bucketIndex];
//将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry
table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
//若HashMap中元素的个数超过极限了,则容量扩大两倍
if (size++ >= threshold)
resize(2 * table.length);
}
随着HashMap中元素的数量越来越多,发生碰撞的概率就越来越大,所产生的链表长度就会越来越长,这样势必会影响HashMap的速度,为了保证HashMap的效率,系统必须要在某个临界点进行扩容处理。该临界点在当HashMap中元素的数量等于table数组长度*加载因子。但是扩容是一个非常耗时的过程,因为它需要重新计算这些数据在新table数组中的位置并进行复制处理。所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

读取实现:get(key)
相对于HashMap的存储而言,取就比较简单了。通过key的hash值找到再table数组中的索引处的Entry,然后返回该key对应的value值即可。
public V get(Object key) {
// 若为null,调用getForNullKey方法返回相对应的value
if (key == null)
return getForNullKey();
// 根据该 key 的 hashCode 值计算它的 hash 码
int hash = hash(key.hashCode());
// 取出 table 数组中指定索引处的值
for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k;
//若搜索的key与查找的key相同,则返回相对应的value
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
在这里能够根据key快速的取到value除了和HashMap的数据结构密不可分外,还和Entry有莫大的关系,在前面就提到过,HashMap在存储过程中并没有将key,value分开来存储,而是当做一个整体key-value来处理的,这个整体就是Entry对象。同时value也只相当于key的附属而已。在存储的过程中,系统根据key的hashcode来决定Entry在table数组中的存储位置,在取的过程中同样根据key的hashcode取出相对应的Entry对象。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: HashMap 本身不是线程安全的,如果需要解决线程问题,可以使用下列方法: 1. 加锁:使用 synchronized 关键字或者使用锁对象来保证 HashMap 的操作是线程安全的。 2. 使用 ConcurrentHashMap:这是 Java 并发包提供的线程安全的 HashMap 实现,可以在多线程环境下使用。 例如: ``` Map<String, Integer> map = new ConcurrentHashMap<>(); ``` 如果需要使用加锁的方式,可以如下操作: ``` private final Map<String, Integer> map = new HashMap<>(); private final Object lock = new Object(); public void put(String key, Integer value) { synchronized (lock) { map.put(key, value); } } public Integer get(String key) { synchronized (lock) { return map.get(key); } } ``` ### 回答2: HashMap本身并没有解决线程问题的特性,即它不是线程安全的数据结构。在多线程环境下,如果多个线程同时对HashMap进行读写操作,就有可能导致数据不一致或其他线程安全问题的发生。 然而,我们可以通过以下几种方式来解决HashMap在多线程环境下可能出现的线程问题: 1. 使用线程安全的HashMap类:Java提供了线程安全的HashMap实现,如ConcurrentHashMap。ConcurrentHashMap采用了分段锁的机制,可以并发地对不同的段进行操作,从而提高了并发度和性能。 2. 使用线程安全的包装类:通过使用Collections类的synchronizedMap方法对HashMap进行包装,可以将其转换为线程安全的Map,即使用synchronized关键字对所有方法进行加锁。这样,每次只允许一个线程对HashMap进行读写操作,能够确保线程安全。 3. 使用显式锁:通过使用显式锁(如ReentrantLock或ReadWriteLock)对HashMap进行加锁和解锁操作,可以实现对读写操作的互斥访问,确保线程安全。 4. 限制只读访问:如果只有读操作而没有写操作,可以将HashMap声明为final和不可变,这样就不需要考虑线程安全问题。 总之,HashMap本身是非线程安全的,但我们可以通过使用线程安全的HashMap实现、包装类、显式锁或限制只读访问来解决HashMap在多线程环境下的线程问题。 ### 回答3: HashMap是Java中常用的数据结构,用于存储键值对。在多线程环境下,HashMap可以出现线程问题,如不一致的读写操作,导致数据的丢失或者错乱。 为了解决线程问题,Java提供了ConcurrentHashMap类,它是线程安全的HashMap的实现。ConcurrentHashMap使用一种称为分段锁(Segment)的机制来实现并发访问。 ConcurrentHashMap内部分为多个小的Segment段,每个段都相当于一个独立的小的HashMap,可以独立地进行加锁操作。这样,当一个线程访问某个段时,其他段仍然可以被其他线程访问,不会阻塞其他的操作。 在写入操作时,ConcurrentHashMap只锁住相关的段,而不影响其他段的访问操作。这样就允许多个线程并发地进行读写操作,提高了并发性能。 此外,ConcurrentHashMap使用volatile和CAS(Compare and Swap)操作来保证内存的可见性和原子性,确保数据的一致性。 总结来说,HashMap可以通过ConcurrentHashMap解决线程问题。ConcurrentHashMap使用分段锁机制,允许并发地读写操作,提高了并发性能。并且它使用volatile和CAS操作来保证内存的可见性和原子性,确保数据的一致性。因此,在多线程环境下,推荐使用ConcurrentHashMap而不是普通的HashMap来处理并发访问的问题

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值