并发

ConcurrentHashMap 的工作原理及代码实现

  ConcurrentHashMap采用了非常精妙的"分段锁"策略,ConcurrentHashMap的主干是个Segment数组。Segment继承了ReentrantLock,所以它就是一种可重入锁(ReentrantLock)。在ConcurrentHashMap,一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,对于不同

  

    ConcurrentHashMap采用了非常精妙的"分段锁"策略,ConcurrentHashMap的主干是个Segment数组。Segment继承了ReentrantLock,所以它就是一种可重入锁(ReentrantLock)。在ConcurrentHashMap,一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,对于不同Segment的数据进行操作是不用考虑锁竞争的。

  当问到我们有关于ConcurrentHashMap的工作原理以及实现时,可以从以下几个方面说:

  • ConcurrentHashMap的优点,即HashMap和HashTable的缺点。

  • ConcurrentHashMap两个静态内部类:HsahEntry和segment

  • ConcurrentHashMap含有16个segment

  • ConcurrentHashMap的put方法:根据hash值找到对应的segment,segment是分段锁。

  • ConcurrentHashMap的get方法:count>0,hash找到HashEntry,hash相等并且key相同,若取value为null,加锁重新获取。

  • ConcurrentHashMap的remove方法:加锁,每删除一个元素就将那之前的元素克隆一边。因为设置为第一次next之后不能再改变。

  • ConcurrentHashMap的size()方法:2次不锁住segment方式统计各个segment的大小,若count发生变化,采用加锁方式统计。modCount变量,在put,remove和clean方法里操作元素,modcount加1.

ConcurrentHashMap是Java1.5中引用的一个线程安全的支持高并发的HashMap集合类。

1、线程不安全的HashMap

因为多线程环境下,使用Hashmap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。

2、效率低下的HashTable

HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。

因为当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态。

如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。

3、锁分段技术

HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁,

那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,

从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术。

首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。

这里“按顺序”是很重要的,否则极有可能出现死锁,在ConcurrentHashMap内部,段数组是final的,并且其成员变量实际上也是final的,

但是,仅仅是将数组声明为final的并不保证数组成员也是final的,这需要实现上的保证。这可以确保不会出现死锁,因为获得锁的顺序是固定的。

oncurrentHashMap 类中包含两个静态内部类 HashEntry 和 Segment。

HashEntry 用来封装映射表的键 / 值对;Segment 用来充当锁的角色,每个 Segment 对象守护整个散列映射表的若干个桶。

每个桶是由若干个 HashEntry 对象链接起来的链表。一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组。

每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。

 

5、HashEntry类

 

复制代码

static final class HashEntry<K,V> { 
            final K key;                 // 声明 key 为 final 型
            final int hash;              // 声明 hash 值为 final 型 
            volatile V value;           // 声明 value 为 volatile 型
            final HashEntry<K,V> next;  // 声明 next 为 final 型  
 
            HashEntry(K key, int hash, HashEntry<K,V> next, V value)  { 
                this.key = key; 
                this.hash = hash; 
                this.next = next; 
                this.value = value; 
            } 
     }

复制代码

 

每个HashEntry代表Hash表中的一个节点,在其定义的结构中可以看到,除了value值没有定义final,其余的都定义为final类型,我们知道Java中关键词final修饰的域成为最终域。

用关键词final修饰的变量一旦赋值,就不能改变,也称为修饰的标识为常量。这就意味着我们删除或者增加一个节点的时候,就必须从头开始重新建立Hash链,因为next引用值需要改变。  

由于 HashEntry 的 next 域为 final 型,所以新节点只能在链表的表头处插入。 例如将A,B,C插入空桶中,插入后的结构为: 

 

6、segment类

 

复制代码

static final class Segment<K,V> extends ReentrantLock implements Serializable {  
     private static final long serialVersionUID = 2249069246763182397L;  
             /** 
              * 在本 segment 范围内,包含的 HashEntry 元素的个数
              * 该变量被声明为 volatile 型,保证每次读取到最新的数据              */  
             transient volatile int count;  
 
 
             /** 
              *table 被更新的次数              */  
             transient int modCount;  
 
 
             /** 
              * 当 table 中包含的 HashEntry 元素的个数超过本变量值时,触发 table 的再散列              */  
             transient int threshold;  
 
 
             /** 
              * table 是由 HashEntry 对象组成的数组
              * 如果散列时发生碰撞,碰撞的 HashEntry 对象就以链表的形式链接成一个链表
              * table 数组的数组成员代表散列映射表的一个桶
              * 每个 table 守护整个 ConcurrentHashMap 包含桶总数的一部分
              * 如果并发级别为 16,table 则守护 ConcurrentHashMap 包含的桶总数的 1/16 
              */  
             transient volatile HashEntry<K,V>[] table;  
 
 
             /** 
              * 装载因子              */  
             final float loadFactor;  
     }

复制代码

Segment 类继承于 ReentrantLock 类,从而使得 Segment 对象能充当锁的角色。每个 Segment 对象用来守护其(成员对象 table 中)包含的若干个桶。

table 是一个由 HashEntry 对象组成的数组。table 数组的每一个数组成员就是散列映射表的一个桶。  

每一个 Segment 对象都有一个 count 对象来表示本 Segment 中包含的 HashEntry 对象的总数。  

之所以在每个 Segment 对象中包含一个计数器,而不是在 ConcurrentHashMap 中使用全局的计数器,是为了避免出现“热点域”而影响 ConcurrentHashMap 

原创不易,完成人机校验,阅读全文

相关推荐