Java ConcurrentHashMap分析

今天项目里面出现了一个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),可以改进性能。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值