ConcurrentHashMap实现原理

ConcurrentHashMap采用了分段加锁的方式
看看get操作hashTable和ConcurrenHashMap的区别
public synchronized V get(Object key) {
        Entry tab[] = table;
        int hash = hash(key);
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return e.value;
            }
        }
        return null;
}
而在ConcurrentHashMap当中,是通过Segment的get操作获取的

问:ConcurrentHashMap的get操作有没有加锁?
答:并没有
问:那怎么实现的并发安全准确的读取数据呢?
答:当出现有key,但是没有value的情况时,将加lock锁,等待value值写入,再读取,防止读不到最新的值。

问:ConcurrentHashMap有哪些参数可以在构造方法中设置?
答:concurrencyLevel 并发度,也就是segment的段数 loadfactor 加载因子,进行扩容的阀值因子 initialCapacity 初始化总的线性链的个数,平均分配到每个segment当中
问:为什么concurrencyLevel 和每个segment当中的链表数都设置成2的n次方形式?
为了根据hash值得前缀或者后缀快速定位所属的segment和线性链

static final class HashEntry<K,V> {
    final K key;
    final int hash;
    volatile V value;
    final HashEntry<K,V> next;
}
HashEntry节点key hash next 都定义成final为什么? volatile修饰value为什么?
为了防止链表结构被破坏,出现ConcurrentModification的情况。

-------------------------------
//返回键值key映射的value值,当不包含key键时返回null
public V get(Object key) {
        int hash = hash(key.hashCode());
        return segmentFor(hash).get(key, hash);
}

 /* Specialized implementations of map methods */

        V get(Object key, int hash) {
            if (count != 0) { // read-volatile
                HashEntry<K,V> e = getFirst(hash);
                while (e != null) {
                    if (e.hash == hash && key.equals(e.key)) {
                        V v = e.value;
                        if (v != null)
                            return v;
                        return readValueUnderLock(e); // recheck
                    }
                    e = e.next;
                }
            }
            return null;
        }

Segment的get操作没有加锁又是怎么实现并发的呢?
ConcurrentHashMap的get操作是直接委托给Segment的get方法

get操作不需要锁。

第一步是访问count变量,这是一个volatile变量,由于所有的修改操作在进行结构修改时都会在最后一步写count变量,通过这种机制保证get操作能够得到最新的结构更新。

下面都是Segment内部的变量和方法

/**
 * The number of elements in this segment's region.
 */
   transient volatile int count;

 /**
 *   Returns properly casted first entry of bin for given hash.
 */
  HashEntry<K,V> getFirst(int hash) {
       HashEntry<K,V>[] tab = table;
       return tab[hash & (tab.length - 1)];
  }

链表节点

static final class HashEntry<K,V> {
        final K key;
        final int hash;
        volatile V value;
        final HashEntry<K,V> next;

        HashEntry(K key, int hash, HashEntry<K,V> next, V value) {
            this.key = key;
            this.hash = hash;
            this.next = next;
            this.value = value;
        }

        @SuppressWarnings("unchecked")
        static final <K,V> HashEntry<K,V>[] newArray(int i) {
            return new HashEntry[i];
        }
    }

对于非结构更新,也就是结点值的改变,由于HashEntry的value变量是volatile的,也能保证读取到最新的值。


接下来就是对hash链进行遍历找到要获取的结点,如果没有找到,直接访回null。对hash链进行遍历不需要加锁的原因在于链指针next是final的


但是头指针却不是final的,这是通过getFirst(hash)方法返回,也就是存在table数组中的值。这使得getFirst(hash)可能返回过时的头结点,


例如,当执行get方法时,刚执行完getFirst(hash)之后,另一个线程执行了删除操作并更新头结点,这就导致get方法中返回的头结点不是最新的。这是可以允许,通过对count变量的协调机制,get能读取到几乎最新的数据,虽然可能不是最新的。要得到最新的数据,只有采用完全的同步。

最后,如果找到了所求的结点,判断它的值如果非空就直接返回,否则在有锁的状态下再读一次。这似乎有些费解,理论上结点的值不可能为空,这是因为put的时候就进行了判断,如果为空就要抛NullPointerException。空值的唯一源头就是HashEntry中的默认值,因为HashEntry中的value不是final的,非同步读取有可能读取到空值。仔细看下put操作的语句:tab[index] = new HashEntry<K,V>(key, hash, first, value),在这条语句中,HashEntry构造函数中对value的赋值以及对tab[index]的赋值可能被重新排序,这就可能导致结点的值为空。这种情况应当很罕见,一旦发生这种情况,ConcurrentHashMap采取的方式是在持有锁的情况下再读一遍,这能够保证读到最新的值,并且一定不会为空值。

posted on 2017-11-09 23:46  贝克田庄  阅读(350)  评论(0编辑  收藏  举报

导航