关闭

Java ConcurrentHashMap分析

339人阅读 评论(0) 收藏 举报
分类:

今天项目里面出现了一个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. 执行删除之后的新链表


从上图可以看出,删除节点 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),可以改进性能。



0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:45390次
    • 积分:821
    • 等级:
    • 排名:千里之外
    • 原创:37篇
    • 转载:5篇
    • 译文:2篇
    • 评论:5条
    博客专栏
    最新评论