• 欢迎访问web前端中文站,JavaScript,CSS3,HTML5,web前端demo
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏web前端中文站吧

java ConcurrentHashMap 教程

JAVA web前端中文站 2年前 (2017-07-06) 815次浏览 未收录 0个评论

ConcurrentHashMap 具体是怎么实现线程安全的呢,肯定不可能是每个方法加 synchronized,那样就变成了 HashTable。

更多精彩内容请看 web 前端中文站
http://www.lisa33xiaoq.net 可按 Ctrl + D 进行收藏

集合是编程中最常用的数据结构。而谈到并发,几乎总是离不开集合这类高级数据结构的支持。比如两个线程需要同时访问一个中间临界区(Queue),比如常会用缓存作为外部文件的副本(HashMap)。这篇文章主要分析 jdk1.5 的 3 种并发集合类型(concurrent,copyonright,queue)中的 ConcurrentHashMap,让我们从原理上细致的了解它们,能够让我们在深度项目开发中获益非浅。

通过分析 Hashtable 就知道,synchronized 是针对整张 Hash 表的,即每次锁住整张表让线程独占,ConcurrentHashMap 允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对 hash 表的不同部分进行的修改。ConcurrentHashMap 内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的 hash table,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。
有些方法需要跨段,比如 size()和 containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁,在 ConcurrentHashMap 内部,段数组是 final 的,并且其成员变量实际上也是 final 的,但是,仅仅是将数组声明为 final 的并不保证数组成员也是 final 的,这需要实现上的保证。这可以确保不会出现死锁,因为获得锁的顺序是固定的。

当资源在多线程下共享时会产生一些逻辑问题,这个时候类或者方法会产生不符合正常逻辑的结果,则不是线程安全的。纵观 jdk 的版本更新,可以看到 jdk 的开发人员在高并发和多线程下了很大的功夫,尽可能的通过 jdk 原生 API 来给开发人员带来最方便最轻松的高并发数据模型,甚至想完全为开发人员解决并发问题,可以看得出来 jdk 的开发人员确实很用心。但是在大量业务数据的逻辑代码的情况下高并发还是不可避免,也不可能完全通过 jdk 原生的并发 API 去解决这些并发问题,开发人员不得不自己去空值在高并发环境下的数据高可用性和一致性。

前面说了 jdk 原生的 API 已经有了很多的高并发产品,在 java.util.concurrent 包下有很多解决高并发,高吞吐量,多线程问题的 API。比如线程池 ThreadPoolExecutor,线程池工厂 Executors,Future 模式下的接口 Future,阻塞队列 BlockingQueue 等等。

数据的可见性

concurrentHashMap 相信用的人也很多,因为在数据安全性上确实比 HashMap 好用,在性能上比 hashtable 也好用。大家都知道线程在操作一个变量的时候,比如 i++,jvm 执行的时候需要经过两个内存,主内存和工作内存。那么在线程 A 对 i 进行加 1 的时候,它需要去主内存拿到变量值,这个时候工作内存中便有了一个变量数据的副本,执行完这些之后,再去对变量真正的加 1,但是此时线程 B 也要操作变量,并且逻辑上也是没有维护多线程访问的限制,则很有可能在线程 A 在从主内存获取数据并在修改的时候线程 B 去主内存拿数据,但是这个时候主内存的数据还没有更新,A 线程还没有来得及讲加 1 后的变量回填到主内存,这个时候变量在这两个线程操作的情况下就会发生逻辑错误。

原子性

原子性就是当某一个线程 A 修改 i 的值的时候,从取出 i 到将新的 i 的值写给 i 之间线程 B 不能对 i 进行任何操作。也就是说保证某个线程对 i 的操作是原子性的,这样就可以避免数据脏读。

volatile 的作用

Volatile 保证了数据在多线程之间的可见性,每个线程在获取 volatile 修饰的变量时候都回去主内存获取,所以当线程 A 修改了被 volatile 修饰的数据后其他线程看到的一定是修改过后最新的数据,也是因为 volatile 修饰的变量数据每次都要去主内存获取,在性能上会有些牺牲。

措施

HashMap 在多线程的场景下是不安全的,hashtable 虽然是在数据表上加锁,纵然数据安全了,但是性能方面确实不如 HashMap。那么来看看 concurrentHashMap 是如何解决这些问题的。

concurrentHashMap 由多个 segment 组成,每一个 segment 都包含了一个 HashEntry 数组的 hashtable, 每一个 segment 包含了对自己的 hashtable 的操作,比如 get,put,replace 等操作(这些操作与 HashMap 逻辑都是一样的,不同的是 concurrentHashMap 在执行这些操作的时候加入了重入锁 ReentrantLock),这些操作发生的时候,对自己的 hashtable 进行锁定。由于每一个 segment 写操作只锁定自己的 hashtable,所以可能存在多个线程同时写的情况,性能无疑好于只有一个 hashtable 锁定的情况。通俗的讲就是 concurrentHashMap 由多个 hashtable 组成。

concurrentHashMap 源码

看下 concurrentHashMap 的 remove 操作

 V remove(Object key, int hash, Object value) {  lock();//重入锁  try {   int c = count - 1;   HashEntry<K,V>[] tab = table;   int index = hash & (tab.length - 1);   HashEntry<K,V> first = tab[index];   HashEntry<K,V> e = first;   while (e != null && (e.hash != hash || !key.equals(e.key)))    e = e.next;    V oldValue = null;   if (e != null) {    V v = e.value;    if (value == null || value.equals(v)) {     oldValue = v;     // All entries following removed node can stay     // in list, but all preceding ones need to be     // cloned.     ++modCount;     HashEntry<K,V> newFirst = e.next;     for (HashEntry<K,V> p = first; p != e; p = p.next)      newFirst = new HashEntry<K,V>(p.key, p.hash,               newFirst, p.value);     tab[index] = newFirst;     count = c; // write-volatile    }   }   return oldValue;  } finally {   unlock();//释放锁  } }

Count 是被 volatile 所修饰,保证了 count 的可见性,避免操作数据的时候产生逻辑错误。segment 中的 remove 操作和 HashMap 大致一样,HashMap 没有 lock()和 unlock()操作。

看下 concurrentHashMap 的 get 源码

 V get(Object key, int hash) {  if (count != 0) { // read-volatile   HashEntry<K,V> e = getFirst(hash);    //如果没有找到则直接返回 null   while (e != null) {    if (e.hash == hash && key.equals(e.key)) {        //由于没有加锁,在 get 的过程中,可能会有更新,拿到的 key 对应的 value 可能为 null,需要单独判断一遍     V v = e.value;        //如果 value 为不为 null,则返回获取到的 value     if (v != null)      return v;     return readValueUnderLock(e); // recheck    }    e = e.next;   }  }  return null; }

关于 concurrentHashMap 的 get 的相关说明已经在上面代码中给出了注释,这里就不多说了。

看下 concurrentHashMap 中的 put

 public V put(K key, V value) {  if (value == null)   throw new NullPointerException();  int hash = hash(key.hashCode());  return segmentFor(hash).put(key, hash, value, false); }

可以看到 concurrentHashMap 不允许 key 或者 value 为 null

接下来看下 segment 的 put

 V put(K key, int hash, V value, boolean onlyIfAbsent) {  lock();  try {   int c = count;   if (c++ > threshold) // ensure capacity    rehash();   HashEntry<K,V>[] tab = table;   int index = hash & (tab.length - 1);   HashEntry<K,V> first = tab[index];   HashEntry<K,V> e = first;   while (e != null && (e.hash != hash || !key.equals(e.key)))    e = e.next;    V oldValue;   if (e != null) {    oldValue = e.value;    if (!onlyIfAbsent)     e.value = value;   }   else {    oldValue = null;    ++modCount;    tab[index] = new HashEntry<K,V>(key, hash, first, value);    count = c; // write-volatile   }   return oldValue;  } finally {   unlock();  } }

同样也是加入了重入锁,其他的基本和 HashMap 逻辑差不多。值得一提的是 jdk8 中添加的中的 putval,这里就不多说了。

总结

ConcurrentHashmap 将数据结构分为了多个 Segment,也是使用重入锁来解决高并发,讲他分为多个 segment 是为了减小锁的力度,添加的时候加了锁,索引的时候没有加锁,使用 volatile 修饰 count 是为了保持 count 的可见性,都是 jdk 为了解决并发和多线程操作的常用手段。

【注:本文源自网络文章资源,由站长整理发布】


web 前端中文站 , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:java ConcurrentHashMap 教程
喜欢 (0)
发表我的评论
取消评论
表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址