并发容器ConcurrentHashMap

HashMap不是线程安全的容器,HashTable是同步的容器,但是在有大量线程竞争存在的时会产生严重的低效率的问题,并发容器可以比较好地解决这个问题。
ConcurrentHashMap将数据分为多段,然后每段数据分别有一把锁,当不同的线程分别访问不同的数据段时,申请的是不同的锁,因此不会产生竞争,大大地提高了访问效率。其实就是通过将数据分段,然后采用锁分段技术。
这里有一个问题: 如果将分段的粒度细分为每个键值对,那不是会最大程度地提高并发效率吗?为什么实际上没有这么做?

  1. Segment的初始化
    通过输入的concurrencyLevel参数,确定Segment的个数ssize: ssize是大于等于concurrencyLevel的数中,最小的为2的指数次方的数。
    然后根据initialCapacity和ssize计算出每个Segment的初始容量,cap;再和loadFactor一起初始化Segment。对每个Segment实施该操作。

由于增加了数据分段操作,那么在查询时就要先定位segment,再在segment里面查询,相比于普通的HashMap,增加了一个segment的hash定位过程。
比如get方法:

public V get(Object key) {
       int hash = hash(key.hashCode());
       return segmentFor(hash).get(key, hash);
   }

首先将key的hashCode返回的值再经过一个hash函数,得到再hash值,然后利用这个再hash值去定位segment。

final Segment<K,V> segmentFor(int hash) {
    return segments[(hash >>> segmentShift) & segmentMask];
}

segmentShift为32-log2(ssize),也就是取hash值的最高的n位和segmentMask相与,作为segment的index。

然后就是get:

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;
}

这里的count是volatile类型变量。这个get里面不需要加锁,也就是允许多线程的同时读。注意到,这里的count保证了如果同时有线程在写该segment,那么也能保证最新的数据可以反映到本次get中。
最后,如果取出的value值对应为null,表明这个键值对正在put的过程中,那么就加锁后再读一次,保证得到的是完整的数据。

然后是put:

V put(K key, int hash, V value, boolean onlyIfAbsent) {
    lock();
    try {
        int c = count;
        if (c++ > threshold) // ensure capacity
            rehash();
        HashEntry<K,V>[] tab = table;
        int index = hash & (tab.length - 1);
        HashEntry<K,V> first = tab[index];
        HashEntry<K,V> e = first;
        while (e != null && (e.hash != hash || !key.equals(e.key)))
            e = e.next;

        V oldValue;
        if (e != null) {
            oldValue = e.value;
            if (!onlyIfAbsent)
                e.value = value;
        }
        else {
            oldValue = null;
            ++modCount;
            tab[index] = new HashEntry<K,V>(key, hash, first, value);
            count = c; // write-volatile
        }
        return oldValue;
    } finally {
        unlock();
    }
}

可以看到这个put是需要对当前的segment加锁的。

还有remove操作:

V remove(Object key, int hash, Object value) {
    lock();
    try {
        int c = count - 1;
        HashEntry<K,V>[] tab = table;
        int index = hash & (tab.length - 1);
        HashEntry<K,V> first = tab[index];
        HashEntry<K,V> e = first;
        while (e != null && (e.hash != hash || !key.equals(e.key)))
            e = e.next;

        V oldValue = null;
        if (e != null) {
            V v = e.value;
            if (value == null || value.equals(v)) {
                oldValue = v;
                // All entries following removed node can stay
                // in list, but all preceding ones need to be
                // cloned.
                ++modCount;
                HashEntry<K,V> newFirst = e.next;
                for (HashEntry<K,V> p = first; p != e; p = p.next)
                    newFirst = new HashEntry<K,V>(p.key, p.hash,
                                                  newFirst, p.value);
                tab[index] = newFirst;
                count = c; // write-volatile
            }
        }
        return oldValue;
    } finally {
        unlock();
    }
}

注意,因为HashEntry里面的next是final的,不能像常规的直接修改next域,需要复制要删除的Entry前面的节点。并将它们链接起来。

另外,size()方法:
需要统计每个segment里面的count,对每个segment,需要两遍遍历segment,对比其modCount是否发生了变化,相同就返回,否则再遍历一遍,还不相同就需要对所有的segment加锁,再一个个遍历。

在jdk1.8.0中,已经看不到上述代码的踪迹,但是思想应该还是一样的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值