HashMap、HashTable、ConcurrentHashMap的区别

1、相关概念

一、Map的概念

javadoc中对Map的解释以下:java

An objectthat maps keys to values . Amap cannot contain duplicatekeys; each key can map to at most one value.

This interface takes the place of the Dictionary class, which was atotally abstract class rather than an interface.

The Map interface provides three collection views, which allow amap's contents to be viewed as a set of keys, collection of values,or set of key-value mappings.数组

 从上可知,Map用于存储“key-value”元素对,它将一个key映射到一个并且只能是惟一的一个value。安全

Map可使用多种实现方式,HashMap的实现采用的是hash表;而TreeMap采用的是红黑树。数据结构

二、HashMap

实现了Map接口,实现了将惟一键隐射到特定值上。容许一个NULL键和多个NULL值。非线程安全。多线程

 

三、HashTable

相似于HashMap,可是不容许NULL键和NULL值,比HashMap慢,由于它是同步的。HashTable是一个线程安全的类,它使用synchronized来锁住整张Hash表来实现线程安全,即每次锁住整张表让线程独占。并发

 

四、ConcurrentHashMap

ConcurrentHashMap容许多个修改操做并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不一样部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不一样的部分,每一个段其实就是一个小的Hashtable,它们有本身的锁。只要多个修改操做发生在不一样的段上,它们就能够并发进行。app

 

JDK1.7的实现

在JDK1.7版本中,ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成,以下图所示:ide

Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面的提到的锁分离技术,而每个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构同样高并发

 

JDK1.8的实现

JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操做,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,可是已经简化了属性,只是为了兼容旧版本。性能

 

 2、解决HashMap的线程安全问题

有两种方法能够解决HashMap的线程安全问题:

  • Java的Collections库中的synchronizedMap()方法
  • 使用ConcurrentHashMap

译者注:其实还有第三种方法,使用Hashtable。不过Hashtable是Java 1.1提供的旧有类,从性能上和使用上都不如其余的替代类,所以已经不推荐使用

 //Hashtable

Map<String, String> normalMap = new Hashtable<String, String>();

 

 

//synchronizedMap

synchronizedHashMap = Collections.synchronizedMap(new HashMap<String, String>());

 

 

//ConcurrentHashMap

concurrentHashMap = new ConcurrentHashMap<String, String>();

 

ConcurrentHashMap

  • 当你程序须要高度的并行化的时候,你应该使用ConcurrentHashMap
  • 尽管没有同步整个Map,可是它仍然是线程安全的
  • 读操做很是快,而写操做则是经过加锁完成的
  • 在对象层次上不存在锁(即不会阻塞线程)
  • 锁的粒度设置的很是好,只对哈希表的某一个key加锁
  • ConcurrentHashMap不会抛出ConcurrentModificationException,即便一个线程在遍历的同时,另外一个线程尝试进行修改。
  • ConcurrentHashMap会使用多个锁

SynchronizedHashMap

  • 会同步整个对象
  • 每一次的读写操做都须要加锁
  • 对整个对象加锁会极大下降性能
  • 这至关于只容许同一时间内至多一个线程操做整个Map,而其余线程必须等待
  • 它有可能形成资源冲突(某些线程等待较长时间)
  • SynchronizedHashMap会返回Iterator,当遍历时进行修改会抛出异常

 

3、 Hashtable 和 HashMap

一、不一样点

这两个类主要有如下几方面的不一样:

   Hashtable和HashMap都实现了Map接口,可是Hashtable的实现是基于Dictionary抽象类。

   在HashMap中,null能够做为键,这样的键只有一个;能够有一个或多个键所对应的值为null。当get()方法返回null值时,便可以表示HashMap中没有该键,也能够表示该键所对应的值为null。所以,在HashMap中不能由get()方法来判断HashMap中是否存在某个键,而应该用containsKey()方法来判断。而在Hashtable中,不管是key仍是value都不能为null。

 

  这两个类最大的不一样在于Hashtable是线程安全的,它的方法是同步了的,能够直接用 在多线程环境中。而HashMap则不是线程安全的。在多线程环境中,须要手动实现同步机制。所以,在Collections类中提供了一个方法返回一个 同步版本的HashMap用于多线程的环境:

1 public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) { 2         return new SynchronizedMap<K,V>(m); 3  }

该方法返回的是一个SynchronizedMap的实例。SynchronizedMap类是定义在Collections中的一个静态内部类。它实现了Map接口,并对其中的每个方法实现,经过synchronized关键字进行了同步控制。

2. 潜在的线程安全问题

上面提到Collections为HashMap提供了一个并发版本SynchronizedMap。这个版本中的方法都进行了同步,可是这并不等于这个类就必定是线程安全的。在某些时候会出现一些意想不到的结果。

以下面这段代码:

1 // shm是SynchronizedMap的一个实例
2 if(shm.containsKey('key')){ 3  shm.remove(key); 4 }

 这段代码用于从map中删除一个元素以前判断是否存在这个元素。这里的 containsKey和reomve方法都是同步的,可是整段代码却不是。考虑这么一个使用场景:线程A执行了containsKey方法返回 true,准备执行remove操做;这时另外一个线程B开始执行,一样执行了containsKey方法返回true,并接着执行了remove操做;然 后线程A接着执行remove操做时发现此时已经没有这个元素了。要保证这段代码按咱们的意愿工做,一个办法就是对这段代码进行同步控制,可是这么作付出 的代价太大。

 

在进行迭代时这个问题更改明显。Map集合共提供了三种方式来分别返回键、值、键值对的集合:

Set<K> keySet();

Collection<V> values();

Set<Map.Entry<K,V>> entrySet();

在这三个方法的基础上,咱们通常经过以下方式访问Map的元素:
1 Iterator keys = map.keySet().iterator(); 2 
3 while(keys.hasNext()){ 4  map.get(keys.next()); 5 }

在这里,有一个地方须要注意的是:获得的keySet和迭代器都是Map中元素的一个“视图”,而不是“副本”。问题也就出如今这里,当一个线程正在迭代Map中的元素时,另外一个线程可能正在修改其中的元素。此时,在迭代元素时就可能会抛出ConcurrentModificationException异常。为了解决这个问题一般有两种方法,一是直接返回元素的副本,而不是视图。这个能够经过

集合类的 toArray()方法实现,可是建立副本的方式效率比以前有所下降,特别是在元素不少的状况下;另外一种方法就是在迭代的时候锁住整个集合,这样的话效率就更低了。

 

4、Hashtable 和ConcurrentHashMap

效率低下的HashTable容器

HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的状况 下HashTable的效率很是低下。由于当一个线程访问HashTable的同步方法时,其余线程访问HashTable的同步方法时,可能会进入阻塞 或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,而且也不能使用get方法来获取元素,因此竞争越激烈效率越低。

锁分段技术

HashTable容器在竞争激烈的并发环境下表现出效率低下的缘由是全部访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把 锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不一样数据段的数据时,线程间就不会存在锁竞争,从而能够有效的提升并发访问效率,这就是 ConcurrentHashMap所使用的锁分段技术,首先将数据分红一段一段的存储,而后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据 的时候,其余段的数据也能被其余线程访问。

 

java5中新增了ConcurrentMap接口和它的一个实现类 ConcurrentHashMap。ConcurrentHashMap提供了和Hashtable以及SynchronizedMap中所不一样的锁机 制。Hashtable中采用的锁机制是一次锁住整个hash表,从而同一时刻只能由一个线程对其进行操做;而ConcurrentHashMap中则是 一次锁住一个桶。ConcurrentHashMap默认将hash表分为16个桶,诸如get,put,remove等经常使用操做只锁当前须要用到的桶。 这样,原来只能一个线程进入,如今却能同时有16个写线程执行,并发性能的提高是显而易见的。

 

上面说到的16个线程指的是写线程,而读操做大部分时候都不须要用到锁。只有在size等操做时才须要锁住整个hash表。

 

在迭代方面,ConcurrentHashMap使用了一种不一样的迭代方式。在这种迭代方式中,当iterator被建立后集合再发生改变就再也不是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据,iterator完成后再将头指针替换为新的数据,这样iterator线程可使用原来老的数据,而写线程也能够并发的完成改变。

相关文章
相关标签/搜索