HashMap、HashTable、ConcurrentHashMap的区别

引言

  • Hashmap和HashTable的区别:
  • 简单的回答:
    • HashMap是非线程安全的,HashTable是线程安全的(synchronized)
    • HashMap内部的Key和Value允许空值,HashTable则都不可以
    • HashMap效率比HashTable效率高
  • Java中的另一个线程安全的与HashMap功能极其类似的类是什么?同样是线程安全,它与HashTable在同步上有什么不同?

下面开始本篇博客:

1.HashMap概述:

众所周知,java中存储数据主要用两种,即数组和链表。先来比较一下二者的区别:

  • 数组:存储空间连续,所以寻址快,查询速度快,但是进行增删的话,需要移动很多元素,效率低下。
  • 链表:存储空间不连续,所以查询慢,增删的时候,只需要移动指针即可,所以增删效率高。

先看一张HashMap的结构图:

由上图可知:

  • HashMap是由数组+链表实现的
  • 数组的初始长度为16,每个元素存储的是链表的头结点
  • 键值所存放的数据结构实际上是一个它内部实现的Entry的内部类,数组实现的,属性有key、value和指向下一个Entry的next
  • 另外:HashMap的hash算法是位运算,并不是取模运算,这样效率更高。
  static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

 

2.HashMap初始化:

hashMap有两种常用的初始化方法:

  1. 不需要参数的初始化方式:
 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//初始数组长度
 static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量
 static final float DEFAULT_LOAD_FACTOR = 0.75f;//加载因子

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

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);
    }

3.HashMap的put操作:


public V put(K key, V value) {  
        if (key == null)  
          return putForNullKey(value);  
        int hash = hash(key.hashCode());  
        int i = indexFor(hash, table.length);  
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
            Object k;  
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
                V oldValue = e.value;  
                e.value = value;  
                e.recordAccess(this);  
                return oldValue;  
            }  
        }        
modCount++;  
        addEntry(hash, key, value, i);  
        return null;  
}

3.1:由源码可以看出:hashMap是允许key为null的,会调用putForNullKey(value)方法:


private V putForNullKey(V value) {  
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {  
            if (e.key == null) {  
                V oldValue = e.value;  
                e.value = value;  
                e.recordAccess(this);  
                return oldValue;  
            }  
        }  
        modCount++;  
        addEntry(0, null, value, 0);  
        return null;  
} 
 
void addEntry(int hash, K key, V value, int bucketIndex) {  
    Entry<K,V> e = table[bucketIndex];  
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
        if (size++ >= threshold)  
            resize(2 * table.length);  
    }

当进入putForNullKey(value)方法的时候,会首先遍历table[0]为链表头的链表,如果存在key为null的数据,就将Value替换成新的value,并返回旧的value,如果没有key为null的数据,就会调用addEntry方法,将这条数据新加入。

put时key为null情况的总结:

  • 先在table[0]的链表中寻找null key,如果有null key就直接覆盖原来的value,返回原来的value; 
  • 如果在table[0]中没有找到,就进行头插,但是要先判断是否要扩容,需要就扩容,然后进行头插,此时table[0]就是新插入的null key Entry了

put操作时key不为null的情况:

  • 由上面的源码可知:
    • 先计算存入key的hash值:
int hash = hash(key.hashCode()); 
  •  通过hash计算存放在数组中的位置:

int i = indexFor(hash, table.length); 
  • 判断key和数组元素位置为i的链表中是否存在相同的键,若相同,覆盖并返回原来的value;若不相同,调用addEntry()进行添加数据
for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
    Object k;  
    if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
        V oldValue = e.value;  
        e.value = value;  
        e.recordAccess(this);  
        return oldValue;  
       }  
}        
modCount++;  
addEntry(hash, key, value, i);  
return null; 
  • 至此,hashmap的put流程就介绍完了

4.HashMap的get操作:

话不多说,先看源码:


public V get(Object key) {  
   if (key == null)  
       return getForNullKey();  
   int hash = hash(key.hashCode());  
   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.equals(k)))
                return e.value;  
        }  
    return null;  
}  
 
private V getForNullKey() {  
   for (Entry<K,V> e = table[0]; e != null; e = e.next) {  
   if (e.key == null)  
     return e.value;  
    }  
    return null;  
} 
  • 如果key为null,调用getForNullKey()方法查询key为null的数据。
  • 如果key不为null,先计算key的hashCode,通过hashcode查询以及最后的equals比较,找到对应的value并返回
  • 需要注意的是,当找不到对应的value时,返回的是null,或者value本身的值就是null。

5.HashMap和HashTable的对比:

  1. hashMap和hashTable的底层实现是相同的,都是数组+链表;
  2. HashMap是线程不安全的,HashTable是线程安全的,因为内部的方法都被synchronized修饰;
  3. HashMap效率相比Hashtable效率要高很多;
  4. HashMap中的key和value允许为null,HashTable都不允许,会空指针异常;
  5. HashMap的初始容量为16,HashTable默认为11;
  6. HashMap扩容时乘2,使用位运算扩容,效率高;HashTable扩容时乘2再加1,使用取模运算;
  7. 二者hash运算源码:

//HashMap的散列函数,这里传入参数为键值对的key
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
} 
//返回hash值的索引,h & (length-1)操作等价于 hash % length操作, 但&操作性能更优
static int indexFor(int h, int length) {
    // length must be a non-zero power of 2
    return h & (length-1);
}
 
//HashTable的散列函数直接在put方法里实现了
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;

6.HashTable和ConCurrentHashMap的对比:

首先先介绍一下ConCurrentHashMap:它是线程安全的HashMap的实现,内部大量利用了volatile、final、cas等lock-free技术来减少锁竞争对于性能的影响。

HashTable内部方法使用的是synchronized关键字,这其实就是对对象加锁,虽然解决了线程安全的问题,但会造成性能的下降;

ConCurrentHashMap算是 对HashTable存在问题的优化:其构造函数如下:默认传入的是16,0.75,16

public ConcurrentHashMap(int paramInt1, float paramFloat, int paramInt2)  {  
    //…
    int i = 0;  
    int j = 1;  
    while (j < paramInt2) {  
      ++i;  
      j <<= 1;  
    }  
    this.segmentShift = (32 - i);  
    this.segmentMask = (j - 1);  
    this.segments = Segment.newArray(j);  
    //…
    int k = paramInt1 / j;  
    if (k * j < paramInt1)  
      ++k;  
    int l = 1;  
    while (l < k)  
      l <<= 1;  
  
    for (int i1 = 0; i1 < this.segments.length; ++i1)  
      this.segments[i1] = new Segment(l, paramFloat);  
  }  
 
public V put(K paramK, V paramV)  {  
    if (paramV == null)  
      throw new NullPointerException();  
    int i = hash(paramK.hashCode()); //这里的hash函数和HashMap中的不一样
    return this.segments[(i >>> this.segmentShift & this.segmentMask)].put(paramK, i, paramV, false);  
}  

ConcurrentHashMap采用了分段锁的设计,上面代码的最后一行可以理解成将一个大的Map拆成多个小的HashTable,在put操作中,会根据hash(paramK.hashCode())来决定具体存放到哪一个segment中。如果查看segment的put操作,我们就会发现内部的同步机制是基于lock操作的,这样就保证了对Map的一部分(Segment)上锁,这样影响的只是将要放入同一个Segment的元素的put操作,在保证同步的时候,锁住的不是整个Map(Hashtable就是如此)。

7.HashTable和ConCurrentHashMap的对比:

1.hashMap线程不安全,ConCurrentHashMap线程安全。

2.HashMap键值允许为null,ConCurrentHashMap不允许。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值