ConcurrentHashMap原理分析(不知道是哪一年的笔记,须要从新整理)

ConcurrentHashMap原理分析

HashTable是一个线程安全的类,它使用synchronnized来锁住整张hash表来实现线程安全,即每次锁住整张表让线程独占。ConcurrentHashMap容许多个修改操做并发进行,其关键在于使用了锁分离的技术。它使用了多个锁来控制对hash表的不一样部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不一样的部分,每一个段其实就是一个小的Hashtable,他们有本身的锁。只要多个修改操做发生在不一样的段上,他们就能够并发进行。html

 

有些方法须要跨段,好比size()和containsValue(),他们可能须要锁定整个表而不只仅是某个段,这须要按顺序锁定全部段,操做完毕后,又按顺序释放全部段的锁。这里的“按顺序”是很重要的,不然极有可能出现死锁,在ConcurrentHashMap内部,段数组是final的,而且其成员变量实际上也是final的,可是,仅仅是将数组声明为final的并不保证数组成员也是final的,这须要实现上的保证。这能够确保不会出现死锁,由于得到锁的顺序是固定的。数组

 

实现原理:安全

ConcurrentHashMap使用分段锁技术,将数据分红一段一段的存储,而后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其余段的数据也能被其余线程访问,可以实现真正的并发访问。以下图是ConcurrentHashMap的内部结构图:多线程

从图中能够看到,ConcurrentHashMap内部分为不少个Segment,每个Segment拥有一把锁,而后每一个Segment(继承ReentrantLock并发

 

static final class Segment<K,V> extends ReentrantLock implements Serializableapp

 

Segment继承了ReentrantLock,代表每一个segment均可以当作一个锁。(ReentrantLock前文已经提到,不了解的话就把当作synchronized的替代者吧)这样对每一个segment中的数据须要同步操做的话都是使用每一个segment容器对象自身的锁来实现。只有对全局须要改变时锁定的是全部的segment。ssh

Segment下面包含不少个HashEntry列表数组。对于一个key,须要通过三次(为何要hash三次下文会详细讲解)hash操做,才能最终定位这个元素的位置,这三次hash分别为:函数

  1. 对于一个key,先进行一次hash操做,获得hash值h1,也即h1 = hash1(key);
  2. 将获得的h1的高几位进行第二次hash,获得hash值h2,也即h2 = hash2(h1高几位),经过h2可以肯定该元素的放在哪一个Segment;
  3. 将获得的h1进行第三次hash,获得hash值h3,也即h3 = hash3(h1),经过h3可以肯定该元素放置在哪一个HashEntry。

ConcurrentHashMap中主要实体类就是三个:ConcurrentHashMap(整个Hash表),Segment(桶),HashEntry(节点),对应上面的图能够看出之间的关系this

/** spa

* The segments, each of which is a specialized hash table

*/  

final Segment<K,V>[] segments;

不变(Immutable)和易变(Volatile)ConcurrentHashMap彻底容许多个读操做并发进行,读操做并不须要加锁。若是使用传统的技术,如HashMap中的实现,若是容许能够在hash链的中间添加或删除元素,读操做不加锁将获得不一致的数据。ConcurrentHashMap实现技术是保证HashEntry几乎是不可变的。HashEntry表明每一个hash链中的一个节点,其结构以下所示:

static final class HashEntry<K,V> {  

     final K key;  

     final int hash;  

     volatile V value;  

     volatile HashEntry<K,V> next;  

 }

在JDK 1.6中,HashEntry中的next指针也定义为final,而且每次插入将新添加节点做为链的头节点(同HashMap实现),并且每次删除一个节点时,会将删除节点以前的全部节点 拷贝一份组成一个新的链,而将当前节点的上一个节点的next指向当前节点的下一个节点,从而在删除之后 有两条链存在,于是能够保证即便在同一条链中,有一个线程在删除,而另外一个线程在遍历,它们都能工做良好,由于遍历的线程能继续使用原有的链。于是这种实现是一种更加细粒度的happens-before关系,即若是遍历线程在删除线程结束后开始,则它能看到删除后的变化,若是它发生在删除线程正在执行中间,则它会使用原有的链,而不会等到删除线程结束后再执行,即看不到删除线程的影响。若是这不符合你的需求,仍是乖乖的用Hashtable或HashMap的synchronized版本,Collections.synchronizedMap()作的包装。

 

而HashMap中的Entry只有key是final的

 

 

static class Entry<K,V> implements Map.Entry<K,V> {

        final K key;

        V value;

        Entry<K,V> next;

        int hash;

 

不变 模式(immutable)是多线程安全里最简单的一种保障方式。由于你拿他没有办法,想改变它也没有机会。
不变模式主要经过final关键字来限定的。在JMM中final关键字还有特殊的语义。Final域使得确保初始化安全性(initialization safety)成为可能,初始化安全性让不可变形对象不须要同步就能自由地被访问和共享。

初始化

先看看ConcurrentHashMap的初始化作了哪些事情,构造函数的源码以下:

public ConcurrentHashMap(int initialCapacity,

                             float loadFactor, int concurrencyLevel) {

        if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)

            throw new IllegalArgumentException();

        if (concurrencyLevel > MAX_SEGMENTS)

            concurrencyLevel = MAX_SEGMENTS;

        // Find power-of-two sizes best matching arguments

        int sshift = 0;

        int ssize = 1;

        while (ssize < concurrencyLevel) {

            ++sshift;

            ssize <<= 1;

        }

        this.segmentShift = 32 - sshift;

        this.segmentMask = ssize - 1;

        if (initialCapacity > MAXIMUM_CAPACITY)

            initialCapacity = MAXIMUM_CAPACITY;

        int c = initialCapacity / ssize;

        if (c * ssize < initialCapacity)

            ++c;

        int cap = MIN_SEGMENT_TABLE_CAPACITY;

        while (cap < c)

            cap <<= 1;

        // create segments and segments[0]

        Segment<K,V> s0 =

            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),

                             (HashEntry<K,V>[])new HashEntry[cap]);

        Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];

        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]

        this.segments = ss;

}

传入的参数有initialCapacity,loadFactor,concurrencyLevel这三个。

  • initialCapacity表示新建立的这个ConcurrentHashMap的初始容量,也就是上面的结构图中的Entry数量。默认值为static final int DEFAULT_INITIAL_CAPACITY = 16;
  • loadFactor表示负载因子,就是当ConcurrentHashMap中的元素个数大于loadFactor * 最大容量时就须要rehash,扩容。默认值为static final float DEFAULT_LOAD_FACTOR = 0.75f;
  • concurrencyLevel表示并发级别,这个值用来肯定Segment的个数,Segment的个数是大于等于concurrencyLevel的第一个2的n次方的数。好比,若是concurrencyLevel为12,13,14,15,16这些数,则Segment的数目为16(2的4次方)。默认值为static final int DEFAULT_CONCURRENCY_LEVEL = 16;。理想状况下ConcurrentHashMap的真正的并发访问量可以达到concurrencyLevel,由于有concurrencyLevel个Segment,假若有concurrencyLevel个线程须要访问Map,而且须要访问的数据都刚好分别落在不一样的Segment中,则这些线程可以无竞争地自由访问(由于他们不须要竞争同一把锁),达到同时访问的效果。这也是为何这个参数起名为“并发级别”的缘由。

初始化的一些动做:

  1. 验证参数的合法性,若是不合法,直接抛出异常。
  2. concurrencyLevel也就是Segment的个数不能超过规定的最大Segment的个数,默认值为static final int MAX_SEGMENTS = 1 << 16;,若是超过这个值,设置为这个值。
  3. 而后使用循环找到大于等于concurrencyLevel的第一个2的n次方的数ssize,这个数就是Segment数组的大小,并记录一共向左按位移动的次数sshift,并令segmentShift = 32 - sshift,而且segmentMask的值等于ssize - 1,segmentMask的各个二进制位都为1,目的是以后能够经过key的hash值与这个值作&运算肯定Segment的索引。
  4. 检查给的容量值是否大于容许的最大容量值,若是大于该值,设置为该值。最大容量值为static final int MAXIMUM_CAPACITY = 1 << 30;。
  5. 而后计算每一个Segment平均应该放置多少个元素,这个值c是向上取整的值。好比初始容量为15,Segment个数为4,则每一个Segment平均须要放置4个元素。
  6. 最后建立一个Segment实例,将其当作Segment数组的第一个元素。

 

 

http://www.importnew.com/22007.html

https://my.oschina.net/hosee/blog/639352

相关文章
相关标签/搜索