CurrentHashMap(基于JDK7);分段锁

这两天学习锁时,明白了一个道理:

除了重量级锁以及数据库基本的读写锁之外,其他所有锁的出现都是为了优化这两种基本的锁,提高这两种锁的效率

也不知道总结的对不对,我们暂且按照这个思路进行分析

我们知道HashTable是使用重量级锁sync来保证线程安全的,性能过于低下,所以就出现了分段锁

分段锁就是把一整个锁分成若干份,让多线程竞争锁时,可以同时在不同分段竞争,相当于对线程分流,从而提高并发效率,这就是currentHashMap使用的锁机制

1,将数据分成一段一段地存储

2,给每一段数据配一把锁

3,当一个线程占用锁访问其中一段数据时,其他段数据也能被其他线程访问

CurrentHashMap锁是通过继承Segment类实现(Segment是通过继承ReentrantLock类实现的),也就是Segment是一种可重入锁,

CurrentHashMap是一种双数组结构,首先是一个Segment数组(默认大小为16,也就是初始并发度为16),然后每个Segment数组中包含一个HashEntry数组,然后每个HashEntry后面是一个链表结构,如图:

get()操作

Segment的get操作实现非常简单和高效. 
- 先经过一次再散列 
- 然后使用这个散列值通过散列运算定位到Segment 
- 再通过散列算法定位到元素.

整个get方法不需要加锁,只需要计算两次hash值,然后遍历一个单向链表

之所以不需要加锁是因为:get方法将要使用的共享变量都定义成了volatile类型, 如用于统计当前Segement大小的count字段和用于存储值的HashEntry的value.定义成volatile的变量,能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值,但是只能被单线程写

在get操作里只需要读不需要写共享变量count和value,所以可以不用加锁. 
之所以不会读到过期的值,是因为根据Java内存模型的happen before原则,对volatile字段的写操作先于读操作,即使两个线程同时修改和获取 volatile变量,get操作也能拿到最新的值, 这是用volatile替换锁的经典应用场景.

transient volatile int count;
volatile V value;

 

在定位元素的代码里可以发现,定位HashEntry和定位Segment的散列算法虽然一样,都与数组的长度减去1再相“与”,但是相“与”的值不一样

定位Segment使用的是元素的hashcode再散列后得到的值的高位
定位HashEntry直接使用再散列后的值.
其目的是避免两次散列后的值一样,虽然元素在Segment里散列开了,但是却没有在HashEntry里散列开.

hash >>> segmentShift & segmentMask   // 定位Segment所使用的hash算法
int index = hash & (tab.length - 1);   // 定位HashEntry所使用的hash算法

put()操作

由于需要对共享变量进行写操作,所以为了线程安全,在操作共享变量时必须加锁. 
put方法首先定位到Segment,然后在Segment里进行插入操作. 
插入操作需要经历两个步骤

判断是否需要对Segment里的HashEntry数组进行扩容
定位添加元素的位置,然后将其放在HashEntry数组里

1,是否需要扩容 
在插入元素前会先判断Segment里的HashEntry数组是否超过容量(threshold),如果超过阈值,则对数组进行扩容. 
值得一提的是,Segment的扩容判断比HashMap更恰当,因为HashMap是在插入元素后判断元素是否已经到达容量的,如果到达了就进行扩容,但是很有可能扩容之后没有新元素插入,这时HashMap就进行了一次无效的扩容.
2,如何扩容 
在扩容的时候,首先会创建一个容量是原来两倍的数组,然后将原数组里的元素进行再散列后插入到新的数组里. 
为了高效,ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment扩容.


put方法的第一步,计算segment数组的索引,并找到该segment,然后调用该segment的put方法。
put方法第二步,在Segment的put方法中进行操作。

size()操作

每个 Segment 维护了一个 count 变量来统计该 Segment 中的键值对个数。

/**
 * The number of elements. Accessed only either within locks
 * or among other volatile reads that maintain visibility.
 */
transient int count;

在执行 size 操作时,需要遍历所有 Segment 然后把 count 累计起来。

ConcurrentHashMap 在执行 size 操作时先尝试不加锁,如果连续两次不加锁操作得到的结果一致,那么可以认为这个结果是正确的。

尝试次数使用 RETRIES_BEFORE_LOCK 定义,该值为 2,retries 初始值为 -1,因此尝试次数为 3。

如果尝试的次数超过 3 次,就需要对每个 Segment 加锁。

/**
 * Number of unsynchronized retries in size and containsValue
 * methods before resorting to locking. This is used to avoid
 * unbounded retries if tables undergo continuous modification
 * which would make it impossible to obtain an accurate result.
 */
static final int RETRIES_BEFORE_LOCK = 2;

public int size() {
    // Try a few times to get accurate count. On failure due to
    // continuous async changes in table, resort to locking.
    final Segment<K,V>[] segments = this.segments;
    int size;
    boolean overflow; // true if size overflows 32 bits
    long sum;         // sum of modCounts
    long last = 0L;   // previous sum
    int retries = -1; // first iteration isn't retry
    try {
        for (;;) {
            // 超过尝试次数,则对每个 Segment 加锁
            if (retries++ == RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    ensureSegment(j).lock(); // force creation
            }
            sum = 0L;
            size = 0;
            overflow = false;
            for (int j = 0; j < segments.length; ++j) {
                Segment<K,V> seg = segmentAt(segments, j);
                if (seg != null) {
                    sum += seg.modCount;
                    int c = seg.count;
                    if (c < 0 || (size += c) < 0)
                        overflow = true;
                }
            }
            // 连续两次得到的结果一致,则认为这个结果是正确的
            if (sum == last)
                break;
            last = sum;
        }
    } finally {
        if (retries > RETRIES_BEFORE_LOCK) {
            for (int j = 0; j < segments.length; ++j)
                segmentAt(segments, j).unlock();
        }
    }
    return overflow ? Integer.MAX_VALUE : size;
}

JDK1.8的改动:

JDK 1.7 使用分段锁机制来实现并发更新操作,核心类为 Segment,它继承自重入锁 ReentrantLock,并发度与 Segment 数量相等。

JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败时使用内置锁 synchronized。

并且 JDK 1.8 的实现也在链表过长时会转换为红黑树。

参考链接:https://blog.csdn.net/qq_33589510/article/details/79962152

猜你喜欢

转载自blog.csdn.net/ailaojie/article/details/89004423