HashMap Hashtable和ConcurrentHashMap

简介

HashMap是Hashtable的轻量级实现(非线程安全的实现),他们都完成了Map接口,主要区别在于HashMap允许空(null)键值(key),由于非线程安全,效率上可能高于Hashtable,HashMap允许将Null作为一个entryde key 或者value,而Hashtable不允许 HashMap把Hashtable的contains思路方法去掉了,改成containsvalue和containsKey因为contains思路方法容易让人引起误解Hashtable继承自Dictionary类,而HashMap是Java1.2引进的Map interface的一个实现。

最大的区别是,Hashtable的思路方法是Synchronize的,而HashMap不是,在多个线程访问Hashtable时,不需要自己为它的思路方法实现同步,而HashMap 就必须为的提供外同步。

另外,Hashtable和HashMap采用的hash/rehash算法都大概一样,所以性能不会有很大的差异。

结构

HashMap和Hashtable是Java开发常用的一种集合。两者的比较是Java面试中的常见问题。查看源码知道,Hashtable存在很久了,@since JDK1.0。
HashMap和Hashtable都实现了Map接口,继承结构如下。

Map
├Hashtable
├HashMap
└WeakHashMap

区别

开发中要用哪个还是要根据它们之间的区别来决定。
下面看下HashMap和Hashtable的区别。

null

HashMap可以接受为null的键值(key)和值(value),比如:

	public void testHashMapAndHashtable() {
		HashMap map = new HashMap<String, String>();
		map.put(null, "null");
		map.put("zw", "zw");
		map.put("null", "null2");
		map.put("null2", null );
		map.putIfAbsent("null", "null3");
	
		System.out.println("==elements    == " + map.entrySet());
		
		System.out.println("==HashMap size== " + map.size());
	}
输出:

==elements    == [null=null, null=null2, zw=zw, null2=null]
==HashMap size== 4
而Hashtable则不行,当我们尝试向Hashtable 实例中添加 null为 key或者 value时均会出现NullPointerException 。
	private void printHashtable() {
		Hashtable table = new Hashtable<String, String>();
		table.put("zw", "zw");
//		table.put(null, "null"); // java.lang.NullPointerException
//		table.put("null", null); // java.lang.NullPointerException
	}
查看Hashtable 的 put() 方法,发现该方法有着一些限制。

public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable {
    /** 省略代码 */ 

    public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            //  value 为 null 直接抛出异常
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;

        //  key 为 null 时,调用hashCode() 函数也会 NullPointerException。
        int hash = key.hashCode(); 
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }

        addEntry(hash, key, value, index);
        return null;
    }
    /** 省略代码 */  
}

synchronized

上面的源码中,Hashtable 的 put() 方法加了synchronized, 可知Hashtable是synchronized , 而 HashMap是非synchronized。
因此多个线程可以直接共享一个Hashtable,而不用自己考虑同步问题。很多帖子说Hashtable是遗留类。此外,Java 还提供了ConcurrentHashMap(@since 1.5) 作为 HashTable的替代。 注意是替代 HashTable。ConcurrentHashMap 是不能接受为null的键值(key)或值(value)的,否则抛出java.lang.NullPointerException。
只需要单一线程时,使用HashMap性能要好过Hashtable。如果要使用同步的HashMap,通常做法如下:

Map m = Collections.synchronizeMap(hashMap);

迭代器与fail-fast

这里解释下:

fail-fast,也就是“”快速失败“,它是Java集合的一种错误检测机制。某个线程在对collection进行迭代时,不允许其他线程对该collection进行结构上的修改。比如:线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(添加删除),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast。
说到集合不能不提的一个就是迭代器(Iterator)。先看HashMap的:

final class KeySet extends AbstractSet<K> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        public final Iterator<K> iterator()     { return new KeyIterator(); }
}
HashMap使用 KeyIterator , 对于KeyIterator 代码中有如下检测

if (modCount != expectedModCount)
// 迭代器每次的hasNext()和next()方法都会检查该"mode"是否被改变,当检测到被修改时,抛出Concurrent Modification Exception
    throw new ConcurrentModificationException();
至于HashTable,代码如下:

private class KeySet extends AbstractSet<K> {
        public Iterator<K> iterator() {
            return getIterator(KEYS);
        }
    }
    private <T> Iterator<T> getIterator(int type) {
        if (count == 0) {//                      count: The total number of entries in the hash table.
            return Collections.emptyIterator();
        } else {
            return new Enumerator<>(type, true);// 因此当HashTable 有元素时,使用的是 Enumerator。
        }
    }
而Enumerator 是快速失败的。下面是我的测试代码:

	private void run4FailFast() {
//		HashMap<String,String> nameCollect = new HashMap<String,String>(); 
//		Hashtable<String,String> nameCollect = new Hashtable<String,String>(); 
//		WeakHashMap<String,String> nameCollect = new WeakHashMap<String,String>();
		ConcurrentHashMap<String,String> nameCollect = new ConcurrentHashMap<String,String>(); 
		
		nameCollect.put("zhangsan", "zhangsan");  
		nameCollect.put("lisi", "lisi");  
		nameCollect.put("wangwu","wangwu");  
        Iterator iterator = nameCollect.keySet().iterator();  
        while (iterator.hasNext())  
        {  
            System.out.println(nameCollect.get(iterator.next()));  
            nameCollect.put("zhaoliu", "zhaoliu");  
        }  
	}
测试中,只有 ConcurrentHashMap 能正常输出,其余均会抛出 java.util.ConcurrentModificationException。
因此可知:Hashtable 和 HashMap 都是快速失败的。

容量

对于集合来说,都会涉及容量的概念。之前也讲过 ArrayList 和 LinkedList 的容量。

Hashtable 默认容量是 11, 加载因子是 0.75

    public Hashtable() {
        this(11, 0.75f);
    }
而 HashMap 默认是 16, :

/** 省略代码 */ 
    /**
     * The default initial capacity - MUST be a power of two.
     */
    // 默认容量 2^4 = 16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    /** 省略代码 */ 
    /**
     * The load factor used when none specified in constructor.
     */
    // 默认加载因子 0.75f
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    
    /** 省略代码 */ 
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

原理

这里以HashMap 为例:

首先是一个数组,即桶。每个桶都会装有 0到多个元素。每个桶中元素都是一个链表。对于HashMap , 我们最常用的就是 put() 和get() 方法。


put

对于 put() 方法,有如下几个步骤:
1. 调用 key 的 hashCode(), (返回的hashCode用于找到bucket位置来储存Entry对象)。
2. 根据返回的hashCode, 计算数组中的位置 i。
3. 根据返回的hashCode, 在 i 出遍历链表找到存储 key 的位置。
4. 如果key 已经存在(equals方法), 旧值覆盖新值。
5. 如果不存在,表头插入。

源码如下(这里我的是 JDK 1.6, 不同版本代码可能有所不同,不过原理一样):

    public V put(K key, V value) {
        if (key == null)
            // 若 key为null,调用putForNullKey方法,保存null 在 table第一个位置中。
            return putForNullKey(value);
        // 计算key的hash值
        int hash = hash(key.hashCode());
        // 根据 hash 值,计算key 的hash 值在 table 数组中的位置
        int i = indexFor(hash, table.length);
        // 在 table[i] 处,对链表进行遍历,以便找到保持 key的位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;

            // 如果链表上有 节点的 hash 值与 key.hash 相同 且 key 相等 (equals)
            // 新值替换旧值,并返回
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        // 没有找到位置,将key、value添加至i位置处。
        addEntry(hash, key, value, i);
        return null;
    }
根据 put() 方法可知,该方法大的消耗是对链表的顺序遍历。

根据addEntry() 可知,是在表头添加。

    void addEntry(int hash, K key, V value, int bucketIndex) {
        // 获取 bucketIndex 处链表
        Entry<K,V> e = table[bucketIndex];
        // 将新的 entry 放在表头, 同时让原来的 entry 链接在新的 entry后。
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        // 元素的个数 大于threshold,进行扩容
        if (size++ >= threshold)
            // 这里 threshold 等于 newCapacity * loadFactor,容量扩大两倍
            resize(2 * table.length);
    }
显而易见,这里的 put 最后的结果很大程度上与 key 的 hashCode() 和 equals() 方法有关。

get

get方法的逻辑很相似:

1. 根据hashCode 计算table中的位置。

2. 对算出的位置中的entry进行遍历。

源码如下:

    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        // 利用 hashCode 计算 hash 码
        int hash = hash(key.hashCode());
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            // 如果 hash 值相等, 且 key 相同 (equals方法), 返回相应的value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }
根据get() 方法源码可知,该方法最大的消耗是对链表的遍历。
因此理想情况下每个桶里面只有一个元素时,读取速度最快,且不浪费空间。不过这显然是极端情况。

rehashing

我们知道,当一个map填满了75%的bucket时候, 系统对HashMap进行大小的两倍扩容,同时把内容copy到新map中。
随着HashMap中元素的数量越来越多,就会发生越来越多的碰撞,所产生的链表长度就会越来越长,根据上面的分析,可知这样必会影响HashMap的速度,因此,为了保证HashMap的效率,系统必须要在某个临界点进行扩容处理。
具体做法是:
1. 当元素个数 > 容量 * 加载因子, 扩容两倍。
2. 重新计算位置,并复制。

    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable);
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }
可见这是一个非常耗时的过程。因此好的做法,是初始化时,选择合适的大小。
由此可知,Hashtable有两个影响性能的参数: 初始容量 和 加载因子。
另外这里补充一下:

    static int indexFor(int h, int length) {
        return h & (length-1);
    }

上面put() 和 get() 过程都使用了这个函数。其实这个函数就是计算索引的。为了使table中元素均匀分布,最易想到的做法就是取模,可以取模消耗很大。不过这里利用按位与的方式,因为length 每次都是2 的N次方,所以结果相当于取模。有兴趣的同学可以自己研究。


ConcurrentHashMap

Java 1.5的时候新加了 ConcurrentHashMap 作为 HashTable的替换。由于使用了分段锁技术,并发性比较高。


遍历

SonarLint在扫描时,对Map的遍历有一条Code Smell:

使用entrySet遍历Map类集合KV,而不是keySet方式进行遍历。
说明:keySet其实遍历了2次,一次是转为Interator对象,另一次是从hashMap中取出key对应的value。而entrySet只是遍历一次就把key和value都放到了entry中,效率更好。

    public void traverseMap() {
        Map<String, String> items = new HashMap<>();
        items.put("a", "A");
        items.put("b", "B");
        items.put("c", "C");
        for (Map.Entry<String, String> entry : items.entrySet()) {
            System.out.println("key:" + entry.getKey() + ";value:" + entry.getValue());
        }
    }

到了Java8,也可是使用Map.foreach方法。此时代码更简洁。

    public void traverseMap() {
        Map<String, String> items = new HashMap<>();
        items.put("a", "A");
        items.put("b", "B");
        items.put("c", "C");
        items.forEach((k, v) -> System.out.println("key : " + k + "; value : " + v));
    }

总结

在使用Map类集合K/V是一定要注意能不能存储null的情况。如下:

集合类KeyValueSuper说明
Hashtable不可null不可nullDictionary线程安全
ConcurrentHashMap不可null不可nullAbstractMap分段锁
TreeMap可null可nullAbstractMap线程不安全
HashMap可null可nullAbstractMap线程不安全



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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值