ConcurrentHashMap浅谈

梦想的初衷 提交于 2019-12-04 03:53:36

1、实现原理介绍

ConcurrentHashMap采用分段锁的思想,将整个table分成不同的segment。每个Segment配有一个锁,这样每个段都能并行的做增删改查等功能。


2、ConcurrentHashMap在代码实现上有哪些技巧

个人理解ConcurrentHashMap是比较好的线程安全容器不仅仅是因为它的段锁。还在于他代码中包含了许多好的编程思想。

首先:ConcurrentHashMap在并发访问时采用了CAS技术,所以性能较好。

if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
    == null) { // recheck
    Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
    while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
           == null) {
        if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
            break;
    }
}


其次:ConcurrentHashMap的字段MAX_SCAN_RETRIES,用来限制尝试获得锁的次数。这样可以避免在尝试锁的时候发生修改而一直进行获取锁的操作。

第三:在isEmpty()方法的时候巧妙的使用了modCount字段判断是否为空,并没有添加任何所信息。

public boolean isEmpty() {
   
    long sum = 0L;
    final Segment<K,V>[] segments = this.segments;
//遍历各个段信息
    for (int j = 0; j < segments.length; ++j) {
//获得第j个段
        Segment<K,V> seg = segmentAt(segments, j);
        if (seg != null) {
            if (seg.count != 0)
                return false;
//modCount相加
            sum += seg.modCount;
        }
    }
//如果不为0,继续判断一次每个段是否为空
    if (sum != 0L) { // recheck unless no modifications
        for (int j = 0; j < segments.length; ++j) {
            Segment<K,V> seg = segmentAt(segments, j);
            if (seg != null) {
                if (seg.count != 0)
                    return false;
                sum -= seg.modCount;
            }
        }
//sum加一次、减一次 两次过后看看sum是否为0L。如果不是说明在判断过程中有修改,并且数据非空
        if (sum != 0L)
            return false;
    }
//否则为空
    return true;
}

3、Unsafe对象字段的内存定位。

在ConcurrentHashMap源码中有很多Unsafe方法获得物理地址的代码:从hash值定位段,从段内地址到定位HashEntry地址都需要Unsafe的方法。

Unsafe实例能够做两件事情,

第一、通过Unsafe类可以分配内存,可以释放内存。allocateMemory、reallocateMemory、freeMemory分别用于分配内存,扩充内存和释放内存

第二、可以定位对象某字段的内存位置,也可以修改对象的字段值,即使它是私有的。

static final <K,V> Segment<K,V> segmentAt(Segment<K,V>[] ss, int j) {
    long u = (j << SSHIFT) + SBASE;
    return ss == null ? null :
        (Segment<K,V>) UNSAFE.getObjectVolatile(ss, u);
}
static final <K,V> Segment<K,V> segmentAt(Segment<K,V>[] ss, int j) {
    long u = (j << SSHIFT) + SBASE;
    return ss == null ? null :
        (Segment<K,V>) UNSAFE.getObjectVolatile(ss, u);
}
这些SSHIFT、SBASE就是相对地址偏移量。他们的获得如下:
static {
    int ss, ts;
    try {
        UNSAFE = sun.misc.Unsafe.getUnsafe();
        Class tc = HashEntry[].class;
        Class sc = Segment[].class;
       //arrayBaseOffset方法是一个本地方法,可以获取数组第一个元素的偏移地址
//TBASE 第一个HashEntry的地址偏移量
        TBASE = UNSAFE.arrayBaseOffset(tc);
//SBASE 第一个 Segment的地址偏移量
        SBASE = UNSAFE.arrayBaseOffset(sc);
       //arrayIndexScale是数组中元素的增量地址
        ts = UNSAFE.arrayIndexScale(tc);
        ss = UNSAFE.arrayIndexScale(sc);
//long objectFieldOffset (Field field);    得到 filed在对象中的偏移
        HASHSEED_OFFSET = UNSAFE.objectFieldOffset(
            ConcurrentHashMap.class.getDeclaredField("hashSeed"));
        SEGSHIFT_OFFSET = UNSAFE.objectFieldOffset(
            ConcurrentHashMap.class.getDeclaredField("segmentShift"));
        SEGMASK_OFFSET = UNSAFE.objectFieldOffset(
            ConcurrentHashMap.class.getDeclaredField("segmentMask"));
        SEGMENTS_OFFSET = UNSAFE.objectFieldOffset(
            ConcurrentHashMap.class.getDeclaredField("segments"));
    } catch (Exception e) {
        throw new Error(e);
    }
    if ((ss & (ss-1)) != 0 || (ts & (ts-1)) != 0)
        throw new Error("data type scale not a power of two");
//Segemnt增量按位数据非零的位数
    SSHIFT = 31 - Integer.numberOfLeadingZeros(ss);
//HashEntry增量按位数据非零的值。
    TSHIFT = 31 - Integer.numberOfLeadingZeros(ts);
}


有了上面的说明,我们来看看最终定位一个key值对应的Entry的方法是如何实现的。

段定位:

private Segment<K,V> segmentForHash(int h) {
//hash值向右无符号一定segmentShift个单位获得了h的高32-segmentShift位,然后将该值左移动SSHIFT位+段首地址,获得段偏移地址
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE
//根据段偏移地址和段首信息定位具体的Segment。
    return (Segment<K,V>) UNSAFE.getObjectVolatile(segments, u);
}
Entry定位:
static final <K,V> HashEntry<K,V> entryAt(HashEntry<K,V>[] tab, int i) {
//将hash值右移动 TSHIFT个位置获得HashEntry偏移量 +HashEntry基本地址
 return (tab == null) ? null :(HashEntry<K,V>) UNSAFE.getObjectVolatile
        (tab, ((long)i << TSHIFT) + TBASE);
}

4、为什么说ConcurrentHashMap存在弱一致的问题

jdk1.7版本ConcurrentHashMap已经不存在弱一致性问题了。那么什么是弱一致性问题,jdk1.7又是如何解决的呢?

所谓的弱一致性问题,就是在某些情况下,放置进去的数据不能准确的访问到该数据。下文的put、get方法在(2)、(8)和(3)、(9)这两个地方由于执行顺序的问题会产生某些已经放置进容器的数据没有被正确的获取到。这是因为ConcurrentHashMap的段里面put方法加锁执行、而get方法并没有加锁执行,有可能和put方法在指令执行顺序上即满足happen-before原则又产生了数据的不一致性问题。



如图所示,在上面的执行路径下是无法正确获得放置的数据的。

jdk1.7之后Segement里面的get方法已经被拿掉,接下来,重点来解读下jdk1.7 ConcurrentHashMap的放置和获取数据的过程。

public V get(Object key) {
    Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
//获得该key值的再hash值,该算法散列的更好,减少碰撞
int h = hash(key);
//获得该hash值对应的段偏移量
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
//获得具体段地址
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
        (tab = s.table) != null) {
//获得该段内 该hash值对应的具体HashEntry的地址
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                 (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
             e != null; e = e.next) {
K k;
//如果hash值和key值和需要查询的key一致,则返回该值
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
        }
    }
//否则返回null,未查询到信息
return null;
}
@SuppressWarnings("unchecked")
public V put(K key, V value) {
    Segment<K,V> s;
//如果放置null值,则跑错
if (value == null)
throw new NullPointerException();
//根据key值计算出hash值
int hash = hash(key);
//获得该hash值的段偏移量
int j = (hash >>> segmentShift) & segmentMask;
//根据偏移量计算出具体的短地址   
if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
//首先判断该段数据数量是否超过了段能承受的阈值,超过则扩容将旧信息hash映射过去,没有超过则返回就segment     
s = ensureSegment(j);
//将数据放置到该段的对应hashEntry中去。或者放置table上的HashEntry或者放置到table挂载的hashEntry中去。
return s.put(key, hash, value, false);
}



参考文献:

http://ifeve.com/concurrenthashmap-weakly-consistent/

http://blog.csdn.net/aesop_wubo/article/details/7537278


标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!