Java ConcurrentHashMap分析

原创 2015年11月17日 20:38:31

今天项目里面出现了一个bug.原因是在多线程的环境下使用了HashMap。

HashMap是一个非线程安全的类。

举一个例子,在多线程中,如果有当一个线程在遍历HashMap时,另一个线程执行了put或者remove操作会发生ConcurrentModificationException

public class TestHashMap {
    public static void main(String[] args) {
        final Map<String,String> sessionMap = new HashMap<String,String>();
        for(int i=0;i<3;i++){
             sessionMap.put(i+ "",i+"" );
        }

        Thread t = new Thread(new Runnable(){
            public void run() {
                Iterator<Entry<String, String> iter = sessionMap.entrySet().iterator();
               	while (iter.hasNext()) {
                	Map.Entry<String,String> entry = (Map.Entry<String,String>)iter.next();
                  	try {
                        Thread. sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    String key = (String)entry.getKey();
                    String value = (String)entry.getValue();
            	}
        	} 
      	});
        t.start();
              
      	Thread t2 = new Thread(new Runnable(){
            public void run() {
                try {
                      Thread. sleep(2000);
               } catch (InterruptedException e) {
                      e.printStackTrace();
               }
               sessionMap.remove( "0"); 
            }
      	});   
        t2.start();
    }
}


我们知道可以用线程安全的Hashtable来代替HashMap。但问题是Hashtable的做法是在所有的方法前都加入了synchronized关键字来实现同步
这样效率是很低的,因为所有线程对这个map的任何操作都要竞争这个锁

所以这个时候ConcurrentHashMap就是很好的替代了
首先还是之前的例子 把HashMap换成ConcurrentHashMap后代码可以很好的运行不会有任何错误
那么HashMap是如何实现同步,而又比Hashtable高效呢

JAVA1.7版本:

一。ConcurrentHashMap把整个Hash表切割成了很多块Segment)。
每一个块用一个重入锁保证它的线程安全性。
static final class Segment<K,V> extends ReentrantLock implements Serializable
每一个Segment管理一个hash表
当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
那么具体的的结构如下:


二。ConcurrentHashMap的get()操作是无锁的而且能保证线程一致性。
ConcurrentHashMap中Entry的value值是用volatile关键字修饰的
定义成volatile的变量,能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值。之所以不会读到过期的值,是根据java内存模型的happen before原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get() 操作也能拿到最新的值,这是用volatile替换锁的经典应用场景。——深入分析ConcurrentHashMap 

三。对ConcurrentHashMap进行遍历的时候。
期间其他线程对这个ConcurrentHashMap做了修改(比如执行remove或者put)不会出现错误。
ConcurrentHashMap 中的每一个Entry的hash值,key值,next指针(指向下一个Entry)都是是final关键字定义的。这就保证了一旦通过put()操作往hash表里面插入了一个Entry以后,就无法把它从链表上删除了。

那remove()操作怎么办呢?
它的办法就是——创建一个新的链表。
当我们执行remove()操作的时候。把待删除节点之后的所有节点原样保留在新链表中,把待删除节点之前的每个节点克隆到新链表中。下面通过图例来说明remove()操作假设写线程执行 remove() 操作,要删除链表的 C 节点,另一个读线程同时正在遍历这个链表。

执行删除之前的原链表:
图 4. 执行删除之前的原链表:
执行删除之后的新链表:
图 5. 执行删除之后的新链表

——摘自:探索 ConcurrentHashMap 高并发性的实现机制

从上图可以看出,删除节点 C 之后的所有节点原样保留到新链表中;删除节点 C 之前的每个节点被克隆到新链表中,注意:它们在新链表中的链接顺序被反转了

在执行 remove 操作时,原始链表并没有被修改,也就是说:读线程不会受同时执行 remove 操作的并发写线程的干扰。

综合上面的分析我们可以看出,写线程对某个链表的结构性修改不会影响其他的并发读线程对这个链表的遍历访问。

参考:
jdk1.7源码
《java并发编程实战》
深入分析ConcurrentHashMap 

再谈重入锁


JAVA1.8版本:

java1.8版本中对ConcurrentHashMap做了很大的改变

改进一:取消了单独独立出来的segments字段,而是直接采用Array中每个链表的第一个节点作为锁,从而实现了对每一行数据进行加锁。

以下两行代码提炼自Put()函数

f = tabAt(tab, i = (n - 1) & hash)
synchronized (f)

改进二:将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。对于hash表来说,最核心的能力在于将key hash之后能均匀的分布在数组中。如果hash之后散列的很均匀,那么table数组中的每个队列长度主要为0或者1。但实际情况并非总是如此理想,虽然ConcurrentHashMap类默认的加载因子为0.75,但是在数据量过大或者运气不佳的情况下,还是会存在一些队列长度过长的情况,如果还是采用单向列表方式,那么查询某个节点的时间复杂度为O(n);因此,对于个数超过8(默认值)的列表,jdk1.8中采用了红黑树的结构,那么查询的时间复杂度可以降低到O(logN),可以改进性能。



版权声明:本文为博主原创文章,未经博主允许不得转载。

【Java8源码分析】并发包-ConcurrentHashMap(一)

一、CAS原理简介 Java8中,ConcurrentHashMap摒弃了Segment的概念,而是启用了一种全新的方式实现:利用CAS算法。它沿用了HashMap的思想,底层依然由“数组”+链表+...
  • linxdcn
  • linxdcn
  • 2017年05月26日 21:09
  • 306

Java并发编程之ConcurrentHashMap原理分析

前言: 集合是编程中最常用的数据结构。而谈到并发,几乎总是离不开集合这类高级数据结构的支持。比如两个线程需要同时访问一个中间临界区(Queue),比如常会用缓存作为外部文件的副本(HashMap)。...

【死磕Java并发】-----J.U.C之ConcurrentHashMap红黑树转换分析

原文出处http://cmsblogs.com/ 『chenssy』 在【死磕Java并发】—–J.U.C之Java并发容器:ConcurrentHashMap一文中详细阐述了ConcurrentHa...
  • chenssy
  • chenssy
  • 2017年06月26日 22:32
  • 6232

Java并发容器ConcurrentHashMap原理及HashMap死循环原因的分析

HashMap是我们最常用的数据结构之一,它方便高效,但遗憾的是,HashMap是线程不安全的,在并发环境下,在HashMap的扩容过程中,可能造成散列表的循环锁死。而线程安全的HashTable使用...

Java基础之ConcurrentHashMap原理分析

ConcurrentHashMapHashMap不是线程安全的,而当使用同步集合Collections.synchronizedXXX方法来实现HashMap同步的话,因为它同步的粒度比较大。也就是每...

JAVA源码分析-深度剖析ConcurrentHashMap

还记得大学快毕业的时候要准备找工作了,然后就看各种面试相关的书籍,还记得很多面试书中都说到: HashMap是非线程安全的,HashTable是线程安全的。 那个时候没怎么写Java代码,所以根本...

ConcurrentHashMap源码分析--Java8

如果还停留在锁分离、Segment,那已经out了。 Segment虽保留,但已经简化属性,仅仅是为了兼容旧版本。 CAS算法;unsafe.compareAndSwapInt(this, valu...

ConcurrentHashMap与红黑树实现分析Java8

本文学习知识点 1、二叉查找树,以及二叉树查找带来的问题。 2、平衡二叉树及好处。 3、红黑树的定义及构造。 4、ConcurrentHashMap中红黑树的构造。 在正式分析红黑树之前,有...

Java多线程(三)之ConcurrentHashMap深入分析

一、Map体系 Hashtable是JDK 5之前Map唯一线程安全的内置实现(Collections.synchronizedMap不算)。Hashtable继承的是Diction...

java并发容器ConcurrentHashMap源码分析

本文详细分析了ConcurrentHashMap的实现原理,首先先回顾了HashMap的相关知识,然后从ConcurrentHashMap的构造方法,内部数据结构,Segment数据结构讲解与分析,然...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Java ConcurrentHashMap分析
举报原因:
原因补充:

(最多只允许输入30个字)