ConcurrentHashMap(1.7版本)

1,644 阅读15分钟

初了解&说明

  • ConcurrentHashMap是java并发工具包的其中一个类,该类本身并发安全,可以用于并发环境之中。
  • 全文若没有特殊说明,ConcurrentHashMap默认指代1.7版本。

前置知识

  • 建议先学习HashMap的1.8,1.7版本。
  • 建议先学习ConcurrentHashMap的1.8版本,1.8机制与1.7不太相同,且复杂的多,学完1.8再学1.7会比较轻松。

问题和解答

  • 可以在这个章节笼统的了解一下ConcurrentHashMap。

一、ConcurrentHashMap的java7版本和java8版本有什么区别?

  • 1.8版本后出于1.7,理论上应该是对1.7版本的进一步改进,实际上也是这个样子,1.8版本的并发和性能优于1.7,相对的代码也更复杂。
  • 1.8版本数据结构是哈希表+链表+红黑树结构,1.7 版本只用了哈希表+链表结构。
  • 1.8版本的同步使用的是Synchronize+CAS进行同步,粒度小,可以最大程度的并发;1.7版本使用的是分段所,对一个片段进行加锁,粒度相对大,逻辑相对简单一些。

原理

键值存储

  • ConcurrentHashMap的键值存储和HashMap的1.7版本相似,都是计算哈希,映射到索引,数组中该索引槽位为空则放入,若不为空(发生冲突)则按链式结构存储。

并发控制

ConcurrentHashMap的并发控制用到的是片段锁。ConcurrentHashMap拥有一个片段数组,这个片段数组的长度决定了并发的最大数量。每个片段槽位维护着一个独立的哈希表,当要存键值对时,先映射到指定的片段槽位,再存进该槽位的哈希表中。

由此可知,ConcurrentHashMap维护着多个哈希表,而哈希表的数量则是片段数组的长度,而上锁也是对某个哈希表上锁,对其他哈希表没有影响。类似通过拆分哈希表的方式降低锁的粒度,但代价就是要维护多个哈希表。

源码解析

核心内部类讲解

  • HashEntry 基本的键值存储单位,同时包含了单向链式结构的属性。
  • Segment 片段数组的基本单位,该类维护了独立的哈希表,并负责了锁的操作。

HashEntry<K,V>

/**
 * 保存了键值
 * 本身是链式结构
 */
static final class HashEntry<K,V> {
    final int hash; // 存储哈希,避免总是重复计算
    final K key;  // 键
    volatile V value;  // 值
    volatile HashEntry<K,V> next; // 链式结构,下一个节点

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

    final void setNext(HashEntry<K,V> n) {
        // 通过Unsafe类的指针设置next属性,
        // 确保了其他线程的可见性
        UNSAFE.putOrderedObject(this, nextOffset, n);
    }

    static final sun.misc.Unsafe UNSAFE;
    static final long nextOffset; // next属性的指针
    
    /**
     * 这里都是注册Unsafe类,指针初始化的操作
     */
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class k = HashEntry.class;
            nextOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField("next"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

Segment<K,V>

/**
 * Segment是ConcurrentHashMap并发的核心类。
 * 每个Segment类管理着对应的哈希表,集成了对哈希表的存取值方法和扩容操作。
 * 
 * ConcurrentHashMap通过把哈希表分散到多个Segment中,达到并发的效果,
 * 每个Segment可独立独写,互不影响。
 * Segment的数量决定了最高并发的数量,Segment的数量一旦初始化后面不再改变。
 */
static final class Segment<K,V> extends ReentrantLock implements Serializable {
    private static final long serialVersionUID = 2249069246763182397L;

    /**
     * 获取锁的最大自旋阈值
     * 判断cpu是否支持并发,若不支持则关闭自旋。
     * Runtime.getRuntime().availableProcessors() 该方法用于获取cpu支持的线程数
     */
    static final int MAX_SCAN_RETRIES =
        Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;

    /**
     * 每个片段维护的哈希表
     */
    transient volatile HashEntry<K,V>[] table;

    /**
     * 哈希表中存储的元素数量
     */
    transient int count;

    /**
     * 统计哈希表更改次数。
     * 主要用于统计元素操作,在操作前后分别取值比较,
     * 可判断操作过程中哈希表是否被修改过。
     */
    transient int modCount;

    /**
     * 数组扩容的阈值
     * 当本片段的哈希表元素达到该值,则扩容该哈希表。
     * 该值大小通过 哈希表的长度 * loadFactor 得到。
     * loadFactor在下面介绍。
     */
    transient int threshold;

    /**
     * 扩容因子
     * 该值用于计算上面的属性threshold。
     * 该值通常情况等于0.75.
     */
    final float loadFactor;

    // 构造函数
    Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
        this.loadFactor = lf;
        this.threshold = threshold;
        this.table = tab;
    }

    /**
     * 存键值操作
     * 对当前片段上锁,存值。
     */
    final V put(K key, int hash, V value, boolean onlyIfAbsent) {
        // 尝试上锁
        // 如果上锁失败则先判断该键是否已存在
        // 若存在,则获取到锁后返回该节点
        // 若不存在则获取到锁后返回新创建得节点。
        HashEntry<K,V> node = tryLock() ? null :
            scanAndLockForPut(key, hash, value); 
        V oldValue;
        try {
            HashEntry<K,V>[] tab = table; // 获取该片段对应的哈希表
            int index = (tab.length - 1) & hash; // 计算映射得索引
            HashEntry<K,V> first = entryAt(tab, index); // 获取到哈希表对应得槽位
            for (HashEntry<K,V> e = first;;) {
                if (e != null) {
                    K k;
                    if ((k = e.key) == key ||
                        (e.hash == hash && key.equals(k))) {
                        // 来到这表示找到对应的键值
                        oldValue = e.value; // 保存旧值
                        if (!onlyIfAbsent) {
                            e.value = value; // 设置新值
                            ++modCount; // 修改次数+1
                        }
                        break;
                    }
                    e = e.next;  // 链式遍历
                }
                // 来到这表示插入的键原本不存在
                // 需要新增节点
                else {
                    // 这里的node是尝试上锁失败时创建的
                    // 可以最大程度减少上锁的时间
                    if (node != null)
                        node.setNext(first);
                    else
                        node = new HashEntry<K,V>(hash, key, value, first);
                    int c = count + 1;
                    
                    // 若元素数量达到重组阈值
                    // 而数组长度没达到最大值
                    // 则扩容数组,并插入当前新节点
                    if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                        rehash(node);
                    else
                        setEntryAt(tab, index, node);
                    ++modCount;
                    count = c;
                    oldValue = null;
                    break;
                }
            }
        } finally {
            unlock(); // 解锁
        }
        return oldValue; // 返回旧值,没有则为空
    }
    
    /**
     * 扫描哈希表并上锁
     * 该函数是在对哈希表上锁失败后做的一些预先操作,
     * 可以提前执行上锁后要进行的操作,从而减少上锁的时间。
     * 
     * 该方法在允许自旋的阈值下,判断插入的键是否存在,
     * 若存在则提前找出来,并开始自旋获取锁;
     * 若不存在则创建新节点,然后开始自旋获取锁。
     * 
     * @return node 
     * 若该键存在,返回值必为null
     * 若键不存在,则返回值有可能不为null,完整的扫描了整个哈希表;
     * 也可能为null,扫描到一半获取到了锁。
     */
    private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
        // entryForHash是通过Unsafe类获取到当前片段对应的哈希表
        HashEntry<K,V> first = entryForHash(this, hash);
        HashEntry<K,V> e = first;
        HashEntry<K,V> node = null;
        
        // retires为-1时,则是扫描哈希表,找到或创建键值节点
        // retires大于等于0时,则开始自旋获取锁。
        int retries = -1;
        while (!tryLock()) {
            HashEntry<K,V> f; 
            // 扫描哈希表
            if (retries < 0) {
                // 表示遍历到链表尾部了
                if (e == null) {
                    // 表示该键节点不存在,创建新的节点
                    if (node == null)
                        node = new HashEntry<K,V>(hash, key, value, null);
                    retries = 0;
                }
                // 表示找到对应的键
                else if (key.equals(e.key)) 
                    retries = 0;
                // 链式遍历
                else
                    e = e.next;
            }
            // 这里开始自旋获取锁
            else if (++retries > MAX_SCAN_RETRIES) {
                lock();
                break;
            }
            // 这里判断哈希表是否被更改了
            // 若发生重组,哈希表会被修改。
            else if ((retries & 1) == 0 &&
                     (f = entryForHash(this, hash)) != first) {
                e = first = f; 
                retries = -1; // 重新遍历
            }
        }
        return node;
    }
    
    

    /**
     * 扩容当前片段的哈希表
     * 被调用时已经处于上锁状态,不用考虑当前片段的并发问题。
     *
     * 由于无论是旧的数组容量还是新的数组容量都一定是2的幂次方,
     * 则容量值-1的值用二进制显示是低位全1的掩码,
     * 该掩码可以直接和哈希值做与操作获取到对应的索引。
     * 且新数组的掩码和旧数组的掩码只相差了一个位,
     * 如16-1:00000000000000000000000000001111 
     *   32-1:00000000000000000000000000011111 
     * 因此旧哈希表同一索引槽位的节点重新计算索引,
     * 得到的新索引只可能是两个值。
     * 用上面的例子就是第五位是1还是0,
     * 0则是保持原来的位置
     * 1则去到一个新的索引位置。
     */
    @SuppressWarnings("unchecked")
    private void rehash(HashEntry<K,V> node) {
        HashEntry<K,V>[] oldTable = table;
        int oldCapacity = oldTable.length;
        int newCapacity = oldCapacity << 1; // 新容量大小是原来的两倍
        threshold = (int)(newCapacity * loadFactor); // 新的扩容因子
        HashEntry<K,V>[] newTable =
            (HashEntry<K,V>[]) new HashEntry[newCapacity]; // 新哈希表
        int sizeMask = newCapacity - 1; // 计算新数组掩码。
        
        // 遍历哈希表每个槽位
        for (int i = 0; i < oldCapacity ; i++) {
            HashEntry<K,V> e = oldTable[i];
            if (e != null) {
                HashEntry<K,V> next = e.next;
                int idx = e.hash & sizeMask; // 计算在新数组的索引
                if (next == null) 
                    newTable[idx] = e;
                else {
                    HashEntry<K,V> lastRun = e;
                    int lastIdx = idx;
                    // 获取最后一段不变的节点段
                    for (HashEntry<K,V> last = next;
                         last != null;
                         last = last.next) {
                        int k = last.hash & sizeMask;
                        if (k != lastIdx) {
                            lastIdx = k;
                            lastRun = last;
                        }
                    }
                    // 把最后一段节点放到对应的位置
                    newTable[lastIdx] = lastRun; 
                    // 从头遍历节点,放到指定的槽位
                    for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                        V v = p.value;
                        int h = p.hash;
                        int k = h & sizeMask;
                        HashEntry<K,V> n = newTable[k];
                        // 采用头插法
                        newTable[k] = new HashEntry<K,V>(h, p.key, v, n); 
                    }
                }
            }
        }
        int nodeIndex = node.hash & sizeMask; // 插入新节点
        node.setNext(newTable[nodeIndex]);
        newTable[nodeIndex] = node;
        table = newTable; // 更改默认的哈希表
    }

    /**
     * 移除值
     * 只有当key和value都匹配时才移除
     * 若value为null,则key匹配就移除
     */
    final V remove(Object key, int hash, Object value) {
        if (!tryLock())
            scanAndLock(key, hash); // 自旋上锁
        V oldValue = null;
        try {
            HashEntry<K,V>[] tab = table;
            int index = (tab.length - 1) & hash;
            HashEntry<K,V> e = entryAt(tab, index);
            HashEntry<K,V> pred = null;
            while (e != null) {
                K k;
                HashEntry<K,V> next = e.next;
                if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {
                    V v = e.value;
                    // 如果匹配上
                    if (value == null || value == v || value.equals(v)) {
                        if (pred == null) // 如果要删除的节点是头节点
                            setEntryAt(tab, index, next);
                        else
                            pred.setNext(next);
                        ++modCount;
                        --count;
                        oldValue = v;
                    }
                    break;
                }
                pred = e;
                e = next;
            }
        } finally {
            unlock();
        }
        return oldValue;
    }
    
    /**
     * 自旋获取锁
     * 跟scanAndLockForPut方法很像
     * 个人觉得它的扫描哈希表没啥用。
     * 注释说不管找没找到都要上锁确保更新的顺序一致性,
     * 但它的tryLock是非公平锁。。。
     * 我觉得这个方法很迷。也可能是我很迷。
     */
    private void scanAndLock(Object key, int hash) {
        // entryForHash是通过Unsafe类获取到当前片段对应的哈希表
        HashEntry<K,V> first = entryForHash(this, hash);
        HashEntry<K,V> e = first;
        
        // retires为-1时,则是扫描哈希表,找到或创建键值节点
        // retires大于等于0时,则开始自旋获取锁。
        int retries = -1;  
        
        // 每次循环都尝试获取锁
        while (!tryLock()) {
            HashEntry<K,V> f;
            if (retries < 0) {
                // 表示找到了对应节点
                if (e == null || key.equals(e.key))
                    retries = 0;
                else
                    e = e.next;
            }
            // 若超出自旋阈值则阻塞
            else if (++retries > MAX_SCAN_RETRIES) {
                lock();
                break;
            }
            // 判断默认的哈希表是否被改变。
            else if ((retries & 1) == 0 &&
                     (f = entryForHash(this, hash)) != first) {
                e = first = f; 
                retries = -1; // 重新扫描新哈希表
            }
        }
    }

    /**
     * 替换值
     */
    final boolean replace(K key, int hash, V oldValue, V newValue) {
        // 逻辑与上面相似,不展示。
    }

    /**
     * 
     */
    final V replace(K key, int hash, V value) {
        // 逻辑与上面相似,不展示。
    }

    /**
     * 清除所有元素
     */
    final void clear() {
        // 逻辑很简单,不展示。
    }
}

ConcurrentHashMap控制属性

/**
 * 默认初始化的哈希表大小
 */
static final int DEFAULT_INITIAL_CAPACITY = 16;

/**
 * 默认最高并发大小
 */
static final int DEFAULT_CONCURRENCY_LEVEL = 16;

/**
 * 最大数组容量
 * int类型大小是32位,因此最多左移31位。
 * 但最高位是符号控制位,1<<31得到得是负数。
 *
 * 容量必须是2得幂次方,因此不能低31位全1。
 * 所以最大值只能是1<<30.
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * Segment中哈希表最小容量阈值
 */
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;

/**
 * 最大Segment数量阈值
 */
static final int MAX_SEGMENTS = 1 << 16;

/**
 * 重试次数阈值。
 * 统计元素数量时,若2次计算结果一致,则认为统计得值是正确的
 * 否则则一直重试统计,若超过该值则上锁统计。
 */
static final int RETRIES_BEFORE_LOCK = 2;

ConcurrentHashMap一般属性及初始化

/**
 * Segment的掩码,由Segment数组的容量-1得出。
 */
final int segmentMask;

/**
 * 位移数
 * 通过Segment数量得出。
 * 由于计算键值节点所属的Segment是通过该节点哈希值的高位计算得出,
 * 因此需要把该哈希值右移到掩码的范围,才能进行与操作。
 * 该值就是保存要右移的位数。
 */
final int segmentShift;

/**
 * Segment数组,数组大小既是Segment的数量。
 */
final Segment<K,V>[] segments;

/* 
构造器
这里只展示了核心和较常用的构造器
*/

/**
 * @param initialCapacity 哈希表大小
 * @param loadFactor 加载因子
 * @param concurrencyLevel 并发等级
 */
@SuppressWarnings("unchecked")
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;
    
    /* 初始化Segment属性,数量等 */
    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;
        
    // 只初始化Segment数组的第一个Segment,
    // 其他Segment要用到时才初始化。
    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); //初始化
    this.segments = ss;
}

/**
 * 理论初始化后:
 * Segment数组长度:16
 * 每个哈希表大小:2
 * 扩容因子:0.75
 *
 * 实际上只会初始索引为0的Segment,其他的索引暂不初始化。
 * 其他Segment初始化时,哈希表的大小根据第0个Segment来设置,
 * 而不是按初始值设置。
 */
public ConcurrentHashMap() {
    // this(16, 0.75, 16)
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}

通用方法

/**
 * 计算哈希
 */
private int hash(Object k) {
    int h = hashSeed;

    // 对字符串进一步哈希处理
    if ((0 != h) && (k instanceof String)) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();

    h += (h <<  15) ^ 0xffffcd7d;
    h ^= (h >>> 10);
    h += (h <<   3);
    h ^= (h >>>  6);
    h += (h <<   2) + (h << 14);
    return h ^ (h >>> 16);
}

ConcurrentHashMap存取机制

  • put()
  • get(Object key)

put方法

/**
 * 存储键值
 */
@SuppressWarnings("unchecked")
public V put(K key, V value) {
    Segment<K,V> s;
    // 值不能为null
    if (value == null)
        throw new NullPointerException();
    int hash = hash(key); // 计算哈希
    // 计算该键值对应的Segment
    int j = (hash >>> segmentShift) & segmentMask; 
    // 判断该Segment是否初始化
    if ((s = (Segment<K,V>)UNSAFE.getObject 
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j); // 初始化指定索引Segment
    // 核心上锁、存值在Segment内部实现。(上面内部类已介绍)
    return s.put(key, hash, value, false);
}

/**
 * 初始化给定索引的Segment并返回
 *
 * @param k 索引
 */
@SuppressWarnings("unchecked")
private Segment<K,V> ensureSegment(int k) {
    final Segment<K,V>[] ss = this.segments;
    // 计算内存中指定索引Segment在数组的偏移量
    long u = (k << SSHIFT) + SBASE; 
    Segment<K,V> seg;
    // 判断该Segment有没有初始化
    if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
        // 使用数组第0个Segment作为原型,包括哈希表大小,扩容因子
        Segment<K,V> proto = ss[0];
        int cap = proto.table.length;
        float lf = proto.loadFactor;
        int threshold = (int)(cap * lf);
        HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
        // 再次确认为空
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
            == null) {
            Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
            while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                   == null) {
                // 通过CAS初始化。
                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                    break;
            }
        }
    }
    return seg;
}

get方法

/**
 * 取值
 */
public V get(Object key) {
    Segment<K,V> s; // manually integrate access methods to reduce overhead
    HashEntry<K,V>[] tab;
    int h = hash(key); // 计算哈希
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
    // 获取对应的Segment里的哈希表
    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
        (tab = s.table) != null) {
        // 链式查抄
        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;
            // 判断键是否相等
            if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                return e.value;
        }
    }
    return null; // 找不到返回空
}

计数机制

size方法

/**
 * 统计元素数量
 * 在没有上锁的情况下统计两次,若结果相同则认为正确
 * 否则上锁统计。
 */
public int size() {
    final Segment<K,V>[] segments = this.segments;
    int size;         // 元素数量
    boolean overflow; // 是否溢出
    long sum;         // 对所有Segment修改的次数
    long last = 0L;   // 最近一次的统计结果
    int retries = -1; // 统计次数
    try {
        for (;;) {
            // 判断是否达到重试比较的次数,若是则上锁统计
            if (retries++ == RETRIES_BEFORE_LOCK) {
                // 对所有Segment上锁
                for (int j = 0; j < segments.length; ++j)
                    ensureSegment(j).lock(); 
            }
            sum = 0L;
            size = 0;
            overflow = false;
            // 遍历所有Segment,累加元素数量和修改次数
            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) {
            // 所有segment解锁
            for (int j = 0; j < segments.length; ++j)
                segmentAt(segments, j).unlock();
        }
    }
    return overflow ? Integer.MAX_VALUE : size;
}

总结

分段锁

分段锁就是把一个哈希表拆分成多个哈希表,每个锁管理一个哈希表,从而增加并发量。且每个分段锁独立维护一个哈希表,分段锁内哈希表的扩容也是独立扩容,不会影响其他分段的哈希表。

上锁的时候使用了ReentrantLock尝试上锁,上锁失败会自旋上锁,自旋次数在可并发的CPU中是64次,不可并发时则为1,若在自旋次数中没能获取到锁,则阻塞当前线程。

分段数组一开始只初始化第0个分段,后面的分段等到要用的时候才初始化,且初始化时,哈希表大小和扩容因子以第0个分段的当前值作为原型进行初始化。

存值

存值先通过哈希高位映射出对应的分段,再通过低位映射出对应哈希表的索引,再通过链式处理哈希冲突,从而放值。ConcurrentHashMap不支持存储value为空的值。

取值

ConcurrentHashMap取值不会阻塞,先计算分段,再计算索引,最后取值。

统计数量

在指定阈值范围内不上锁统计,若其中连续两次统计结果一致,则结束返回,若一直不相同且达到了阈值,则全部分段上锁,统计,解锁,返回。