ConcurrentHashMap 实现原理

本贴最后更新于 2436 天前,其中的信息可能已经天翻地覆

ConcurrentHashMap 实现原理

一、java 并发基础

当一个对象或变量可以被多个线程共享的时候,就有可能使得程序的逻辑出现问题。 在一个对象中有一个变量 i=0,有两个线程 A,B 都想对 i 加 1,这个时候便有问题显现出来,关键就是对 i 加 1 的这个过程不是原子操作。要想对 i 进行递增,第一步就是获取 i 的值,当 A 获取 i 的值为 0,在 A 将新的值写入 A 之前,B 也获取了 A 的值 0,然后 A 写入,i 变成 1,然后 B 也写入 i,i 这个时候依然是 1. 当然 java 的内存模型没有上面这么简单,在 Java Memory Model 中,Memory 分为两类,main memory 和 working memory,main memory 为所有线程共享,working memory 中存放的是线程所需要的变量的拷贝(线程要对 main memory 中的内容进行操作的话,首先需要拷贝到自己的 working memory,一般为了速度,working memory 一般是在 cpu 的 cache 中的)。volatile 的变量在被操作的时候不会产生 working memory 的拷贝,而是直接操作 main memory,当然 volatile 虽然解决了变量的可见性问题,但没有解决变量操作的原子性的问题,这个还需要 synchronized 或者 CAS 相关操作配合进行。

可见性

也就说假设一个对象中有一个变量 i,那么 i 是保存在 main memory 中的,当某一个线程要操作 i 的时候,首先需要从 main memory 中将 i 加载到这个线程的 working memory 中,这个时候 working memory 中就有了一个 i 的拷贝,这个时候此线程对 i 的修改都在其 working memory 中,直到其将 i 从 working memory 写回到 main memory 中,新的 i 的值才能被其他线程所读取。从某个意义上说,可见性保证了各个线程的 working memory 的数据的一致性。 可见性遵循下面一些规则:

  • 当一个线程运行结束的时候,所有写的变量都会被 flush 回 main memory 中。
  • 当一个线程第一次读取某个变量的时候,会从 main memory 中读取最新的。
  • volatile 的变量会被立刻写到 main memory 中的,在 jsr133 中,对 volatile 的语义进行增强,后面会提到
  • 当一个线程释放锁后,所有的变量的变化都会 flush 到 main memory 中,然后一个使用了这个相同的同步锁的进程,将会重新加载所有的使用到的变量,这样就保证了可见性。

原子性

还拿上面的例子来说,原子性就是当某一个线程修改 i 的值的时候,从取出 i 到将新的 i 的值写给 i 之间不能有其他线程对 i 进行任何操作。也就是说保证某个线程对 i 的操作是原子性的,这样就可以避免数据脏读。 通过锁机制或者 CAS(Compare And Set 需要硬件 CPU 的支持)操作可以保证操作的原子性。

有序性

假设在 main memory 中存在两个变量 i 和 j,初始值都为 0,在某个线程 A 的代码中依次对 i 和 j 进行自增操作(i,j 的操作不相互依赖)

i++
j++

由于,所有 i,j 修改操作的顺序可能会被重新排序。那么修改后的 ij 写到 main memory 中的时候,顺序可能就不是按照 i,j 的顺序了,这就是所谓的 reordering,在单线程的情况下,当线程 A 运行结束的后 i,j 的值都加 1 了,在线程自己看来就好像是线程按照代码的顺序进行了运行(这些操作都是基于 as-if-serial 语义的),即使在实际运行过程中,i,j 的自增可能被重新排序了,当然计算机也不能帮你乱排序,存在上下逻辑关联的运行顺序肯定还是不会变的。但是在多线程环境下,问题就不一样了,比如另一个线程 B 的代码如下:

if(j==1) {
    System.out.println(i);
}

按照我们的思维方式,当 j 为 1 的时候那么 i 肯定也是 1,因为代码中 i 在 j 之前就自增了,但实际的情况有可能当 j 为 1 的时候 i 还是为 0。这就是 reordering 产生的不好的后果,所以我们在某些时候为了避免这样的问题需要一些必要的策略,以保证多个线程一起工作的时候也存在一定的次序。JMM 提供了 happens-before 的排序策略。JSR 133 Expert Group 决定让 volatile 读写不能与其他内存操作一起重新排序,新的内存模型下,如果当线程 A 写入 volatile 变量 V 而线程 B 读取 V 时,那么在写入 V 时,A 可见的所有变量值现在都可以保证对 B 是可见的。

Happens-before 关系

happens-before 关系保证:如果线程 A 与线程 B 满足 happens-before 关系,则线程 A 执行动作的结果对于线程 B 是可见的。如果两个操作未按 happens-before 排序,JVM 将可以对他们任意重排序。

下面介绍几个与理解 ConcurrentHashMap 有关的 happens-before 关系法则:

  1. 程序次序法则:如果在程序中,所有动作 A 出现在动作 B 之前,则线程中的每动作 A 都 happens-before 于该线程中的每一个动作 B。
  2. 监视器锁法则:对一个监视器的解锁 happens-before 于每个后续对同一监视器的加锁。
  3. Volatile 变量法则:对 Volatile 域的写入操作 happens-before 于每个后续对同一 Volatile 的读操作。
  4. 传递性:如果 A happens-before 于 B,且 B happens-before C,则 A happens-before C。

二、ConcurrentHashMap 实现原理

ConcurrentHashMap 是一个线程安全的 Hash Table,它的主要功能是提供了一组和 HashTable 功能相同但是线程安全的方法。ConcurrentHashMap 可以做到读取数据不加锁,并且其内部的结构可以让其在进行写操作的时候能够将锁的粒度保持地尽量地小,不用对整个 ConcurrentHashMap 加锁。

ConcurrentHashMap 的内部结构

ConcurrentHashMap 为了提高本身的并发能力,在内部采用了一个叫做 Segment 的结构,一个 Segment 其实就是一个类似的 HashTable 的结构,Segment 内部维护了一个链表数组,我们用下面这一幅图来看下 ConcurrentHashMap 的内部结构:

图表 1

或者这样:

图 3.ConcurrentHashMap 的结构示意图:

ConcurrentHashMap 定位一个元素的过程需要进行两次 Hash 操作,第一次 Hash 定位到 Segment,第二次 Hash 定位到元素所在的链表的头部,因此,这一种结构的带来的副作用是 Hash 的过程要比普通的 HashMap 要长,但是带来的好处是写操作的时候可以只对元素所在的 Segment 进行加锁即可,不会影响到其他的 Segment,这样,在最理想的情况下,ConcurrentHashMap 可以最高同时支持 Segment 数量大小的写操作(刚好这些写操作都非常平均地分布在所有的 Segment 上),所以,通过这一种结构,ConcurrentHashMap 的并发能力可以大大的提高。

ConcurrentHashMap 允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对 hash 表的不同部分进行的修改。ConcurrentHashMap 内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的 hashtable,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。

有些方法需要跨段,比如 size()和 containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁,在 ConcurrentHashMap 内部,段数组是 final 的,并且其成员变量实际上也是 final 的,但是,仅仅是将数组声明为 final 的并不保证数组成员也是 final 的,这需要实现上的保证。这可以确保不会出现死锁,因为获得锁的顺序是固定的。不变性是多线程编程占有很重要的地位.

Segment

/** 
 * The segments, each of which is a specialized hash table 
 */  
final Segment<K,V>[] segments; 

我们再来具体了解一下 Segment 的数据结构:

static final class Segment<K,V> extends ReentrantLock implements Serializable {
    transient volatile int count;
    transient int modCount;
    transient int threshold;
    transient volatile HashEntry<K,V>[] table;
    final float loadFactor;
}

详细解释一下 Segment 里面的成员变量的意义:

  • count:Segment 中元素的数量
  • modCount:对 table 的大小造成影响的操作的数量(比如 put 或者 remove 操作)
  • threshold:阈值,Segment 里面元素的数量超过这个值依旧就会对 Segment 进行扩容
  • table:链表数组,数组中的每一个元素代表了一个链表的头部
  • loadFactor:负载因子,用于确定 threshold

count 用来统计该段数据的个数,它是 volatile,它用来协调修改和读取操作,以保证读取操作能够读取到几乎最新的修改。协调方式是这样的,每次修改操作做了结构上的改变,如增加/删除节点(修改节点的值不算结构上的改变),都要写 count 值,每次读取操作开始都要读取 count 的值。这利用了 Java 5 中对 volatile 语义的增强,对同一个 volatile 变量的写和读存在 happens-before 关系。modCount 统计段结构改变的次数,主要是为了检测对多个段进行遍历过程中某个段是否发生改变,在讲述跨段操作时会还会详述。threashold 用来表示需要进行 rehash 的界限值。table 数组存储段中节点,每个数组元素是个 hash 链,用 HashEntry 表示。table 也是 volatile,这使得能够读取到最新的 table 值而不需要同步。loadFactor 表示负载因子。

不变(Immutable)和易变(Volatile)

ConcurrentHashMap 完全允许多个读操作并发进行,读操作并不需要加锁。如果使用传统的技术,如 HashMap 中的实现,如果允许可以在 hash 链的中间添加或删除元素,读操作不加锁将得到不一致的数据。ConcurrentHashMap 实现技术是保证 HashEntry 几乎是不可变的。HashEntry 代表每个 hash 链中的一个节点,其结构如下所示:

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

可以看到除了 value 不是 final 的,其它值都是 final 的,这意味着不能从 hash 链的中间或尾部添加或删除节点,因为这需要修改 next 引用值,所有的节点的修改只能从头部开始。对于 put 操作,可以一律添加到 Hash 链的头部。但是对于 remove 操作,可能需要从中间删除一个节点,这就需要将要删除节点的前面所有节点整个复制一遍,最后一个节点指向要删除结点的下一个结点。这在讲解删除操作时还会详述。为了确保读操作能够看到最新的值,将 value 设置成 volatile,这避免了加锁。

ConcurrentHashMap 初始化

public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
   ///MAX_SEGMENTS 为1<<16=65536,也就是最大并发数为65536
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
  
    // Find power-of-two sizes best matching arguments
    //2的sshif次方等于ssize,例:ssize=16,sshift=4;ssize=32,sshif=5
    int sshift = 0;
    int ssize = 1;
    while (ssize < concurrencyLevel) {
        ++sshift;
        ssize <<= 1;
    }
   //segmentShift和segmentMask这两个变量在定位segment时会用到,后面会详细讲
    segmentShift = 32 - sshift;
    segmentMask = ssize - 1;
    this.segments = Segment.newArray(ssize);
  
  //计算cap的大小,即Segment中HashEntry的数组长度,cap也一定为2的n次方.
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    int c = initialCapacity / ssize;
    if (c * ssize < initialCapacity)
        ++c;
    int cap = 1;
    while (cap < c)
        cap <<= 1;
  
    for (int i = 0; i < this.segments.length; ++i)
        this.segments[i] = new Segment<K,V>(cap, loadFactor);
}

 CurrentHashMap 的初始化一共有三个参数,一个 initialCapacity,表示初始的容量,一个 loadFactor,表示负载参数,最后一个是 concurrentLevel,代表 ConcurrentHashMap 内部的 Segment 的数量,ConcurrentLevel 一经指定,不可改变,后续如果 ConcurrentHashMap 的元素数量增加导致 ConrruentHashMap 需要扩容,ConcurrentHashMap 不会增加 Segment 的数量,而只会增加 Segment 中链表数组的容量大小,这样的好处是扩容过程不需要对整个 ConcurrentHashMap 做 rehash,而只需要对 Segment 里面的元素做一次 rehash 就可以了。

  整个 ConcurrentHashMap 的初始化方法还是非常简单的,先是根据 concurrentLevel 来 new 出 Segment,这里 Segment 的数量是不大于 concurrentLevel 的最大的 2 的指数,就是说 Segment 的数量永远是 2 的指数个,这样的好处是方便采用移位操作来进行 hash,加快 hash 的过程。接下来就是根据 intialCapacity 确定 Segment 的容量的大小,每一个 Segment 的容量大小也是 2 的指数,同样使为了加快 hash 的过程。

  这边需要特别注意一下两个变量,分别是 segmentShift 和 segmentMask,这两个变量在后面将会起到很大的作用,假设构造函数确定了 Segment 的数量是 2 的 n 次方,那么 segmentShift 就等于 32 减去 n,而 segmentMask 就等于 2 的 n 次方减一。

ConcurrentHashMap 的 get 操作

前面提到过 ConcurrentHashMap 的 get 操作是不用加锁的,我们这里看一下其实现:

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

看第三行,segmentFor 这个函数用于确定操作应该在哪一个 segment 中进行,几乎对 ConcurrentHashMap 的所有操作都需要用到这个函数,我们看下这个函数的实现:

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

这个函数用了位操作来确定 Segment,根据传入的 hash 值向右无符号右移 segmentShift 位,然后和 segmentMask 进行与操作,结合我们之前说的 segmentShift 和 segmentMask 的值,就可以得出以下结论:假设 Segment 的数量是 2 的 n 次方,根据元素的 hash 值的高 n 位就可以确定元素到底在哪一个 Segment 中。

在确定了需要在哪一个 segment 中进行操作以后,接下来的事情就是调用对应的 Segment 的 get 方法:

V get(Object key, int hash) {
    /** 
    transient volatile int count;
    对volatile字段的写入操作happens-before于每一个后续的同一个字段的读操作。
  因为实际上put、remove等操作也会更新count的值,所以当竞争发生的时候,volatile的语义可以保证写操作在读操作之前,也就保证了写操作对后续的读操作都是可见的,这样后面get的后续操作就可以拿到完整的元素内容。
  **/
    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;
}

然后,在第三行,调用了 getFirst()来取得链表的头部:

HashEntry<K,V> getFirst(int hash) {
    HashEntry<K,V>[] tab = table;
    return tab[hash & (tab.length - 1)];
}

同样,这里也是用位操作来确定链表的头部,hash 值和 HashTable 的长度减一做与操作,最后的结果就是 hash 值的低 n 位,其中 n 是 HashTable 的长度以 2 为底的结果。在确定了链表的头部以后,就可以对整个链表进行遍历,看第 4 行,取出 key 对应的 value 的值,如果拿出的 value 的值是 null,则可能这个 key,value 对正在 put 的过程中,如果出现这种情况,那么就加锁来保证取出的 value 是完整的,如果不是 null,则直接返回 value。

ConcurrentHashMap 的 put 操作

public V put(K key, V value) {
        Segment<K,V> s;
        //concurrentHashMap不允许key/value为空
        if (value == null)
            throw new NullPointerException();
        //hash函数对key的hashCode重新散列,避免差劲的不合理的hashcode,保证散列均匀
        int hash = hash(key);
        //返回的hash值无符号右移segmentShift位与段掩码进行位运算,定位segment
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);
        return s.put(key, hash, value, false);
    }

从源码看出,put 的主要逻辑也就两步:1.定位 segment 并确保定位的 Segment 已初始化 2.调用 Segment 的 put 方法。

关于 segmentShift 和 segmentMask

segmentShift 和 segmentMask 这两个全局变量的主要作用是用来定位 Segment,int j =(hash >>> segmentShift) & segmentMask。

 segmentMask:段掩码,假如 segments 数组长度为 16,则段掩码为 16-1=15;segments 长度为 32,段掩码为 32-1=31。这样得到的所有 bit 位都为 1,可以更好地保证散列的均匀性

 segmentShift:2 的 sshift 次方等于 ssize,segmentShift=32-sshift。若 segments 长度为 16,segmentShift=32-4=28;若 segments 长度为 32,segmentShift=32-5=27。而计算得出的 hash 值最大为 32 位,无符号右移 segmentShift,则意味着只保留高几位(其余位是没用的),然后与段掩码 segmentMask 位运算来定位 Segment。

V put(K key, int hash, V value, boolean onlyIfAbsent) {
  
    lock();
    try {
        int c = count;
      ////若c超出阈值threshold,需要扩容并rehash。扩容后的容量是当前容量的2倍。这样可以最大程度避免之前散列好的entry重新散列。扩容并rehash的这个过程是比较消耗资源的。
        if (c++ > threshold) // ensure capacity
            rehash();
        HashEntry<K,V>[] tab = table;
      ///定位HashEntry,可以看到,这个hash值在定位Segment时和在Segment中定位HashEntry都会用到,只不过定位Segment时只用到高几位。
        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 {
          // 如果没有找到原来的key,则
          // 生成一个新的HashEntry并且把它加到整个Segment的头部,然后再更新count的值。
            oldValue = null;
            ++modCount;
            tab[index] = new HashEntry<K,V>(key, hash, first, value);
            count = c; // write-volatile
        }
        return oldValue;
    } finally {
        unlock();
    }
}

注意:这里的加锁操作是针对(键的 hash 值对应的)某个具体的 Segment,锁定的是该 Segment 而不是整个 ConcurrentHashMap。因为插入键 / 值对操作只是在这个 Segment 包含的某个桶中完成,不需要锁定整个 ConcurrentHashMap。此时,其他写线程对另外 15 个 Segment(默认 16 个段) 的加锁并不会因为当前线程对这个 Segment 的加锁而阻塞。同时,所有读线程几乎不会因本线程的加锁而阻塞(除非读线程刚好读到这个 Segment 中某个 HashEntry 的 value 域的值为 null,此时需要加锁后重新读取该值)。

ConcurrentHashMap 的 remove 操作

Remove 操作的前面一部分和前面的 get 和 put 操作一样,都是定位 Segment 的过程,然后再调用 Segment 的 remove 方法:

V remove(Object key, int hash, Object value) { 
           lock();         // 加锁
           try{ 
               int c = count - 1; 
               HashEntry<K,V>[] tab = table; 
               // 根据散列码找到 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; 
                       ++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;      // 写 count 变量
                   } 
               } 
               return oldValue; 
           } finally{ 
               unlock();               // 解锁
           } 
       }

和 get 操作一样,首先根据散列码找到具体的链表;然后遍历这个链表找到要删除的节点;最后把待删除节点之后的所有节点原样保留在新链表中,把待删除节点之前的每个节点克隆到新链表中。下面通过图例来说明 remove 操作。假设写线程执行 remove 操作,要删除链表的 C 节点,另一个读线程同时正在遍历这个链表。

删除之前的原链表:

图 4. 执行删除之前的原链表:

删除之后的新链表:

图 5. 执行删除之后的新链表

从上图可以看出,删除节点 C 之后的所有节点原样保留到新链表中;删除节点 C 之前的每个节点被克隆到新链表中,注意:它们在新链表中的链接顺序被反转了。在执行 remove 操作时,原始链表并没有被修改,也就是说:读线程不会受同时执行 remove 操作的并发写线程的干扰。综合上面的分析我们可以看出,写线程对某个链表的结构性修改不会影响其他的并发读线程对这个链表的遍历访问。

整个 remove 实现并不复杂,但是需要注意如下几点。第一,当要删除的结点存在时,删除的最后一步操作要将 count 的值减一。这必须是最后一步操作,否则读取操作可能看不到之前对段所做的结构性修改。第二,remove 执行的开始就将 table 赋给一个局部变量 tab,这是因为 table 是 volatile 变量,读写 volatile 变量的开销很大。编译器也不能对 volatile 变量的读写做任何优化,直接多次访问非 volatile 实例变量没有多大影响,编译器会做相应优化。

ConcurrentHashMap 的 size 方法

有些操作需要涉及到多个段,比如说 size()

public int size() {  
    final Segment<K,V>[] segments = this.segments;  
    long sum = 0;  
    long check = 0;  
    int[] mc = new int[segments.length];  
    // Try a few times to get accurate count. On failure due to  
    // continuous async changes in table, resort to locking.  
    for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) {  
        check = 0;  
        sum = 0;  
        int mcsum = 0;  
        for (int i = 0; i < segments.length; ++i) {  
            sum += segments[i].count;  
            mcsum += mc[i] = segments[i].modCount;  
        }  
        if (mcsum != 0) {  
            for (int i = 0; i < segments.length; ++i) {  
                check += segments[i].count;  
                if (mc[i] != segments[i].modCount) {  
                    check = -1; // force retry  
                    break;  
                }  
            }  
        }  
        if (check == sum)  
            break;  
    }  
    if (check != sum) { // Resort to locking all segments  
        sum = 0;  
        for (int i = 0; i < segments.length; ++i)  
            segments[i].lock();  
        for (int i = 0; i < segments.length; ++i)  
            sum += segments[i].count;  
        for (int i = 0; i < segments.length; ++i)  
            segments[i].unlock();  
    }  
    if (sum > Integer.MAX_VALUE)  
        return Integer.MAX_VALUE;  
    else  
        return (int)sum;  
} 

size 方法主要思路是先在没有锁的情况下对所有段大小求和,如果不能成功(这是因为遍历过程中可能有其它线程正在对已经遍历过的段进行结构性更新),最多执行 RETRIES_BEFORE_LOCK 次,如果还不成功就在持有所有段锁的情况下再对所有段大小求和。在没有锁的情况下主要是利用 Segment 中的 modCount 进行检测,在遍历过程中保存每个 Segment 的 modCount,遍历完成之后再检测每个 Segment 的 modCount 有没有改变,如果有改变表示有其它线程正在对 Segment 进行结构性并发更新,需要重新计算。

ConsurrentHashMap 的 containsValue 实现

public boolean containsValue(Object value) {  
    if (value == null)  
        throw new NullPointerException();  
  
    // See explanation of modCount use above  
  
    final Segment<K,V>[] segments = this.segments;  
    int[] mc = new int[segments.length];  
  
    // Try a few times without locking  
    for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) {  
        int sum = 0;  
        int mcsum = 0;  
        for (int i = 0; i < segments.length; ++i) {  
            int c = segments[i].count;  
            mcsum += mc[i] = segments[i].modCount;  
            if (segments[i].containsValue(value))  
                return true;  
        }  
        boolean cleanSweep = true;  
        if (mcsum != 0) {  
            for (int i = 0; i < segments.length; ++i) {  
                int c = segments[i].count;  
                if (mc[i] != segments[i].modCount) {  
                    cleanSweep = false;  
                    break;  
                }  
            }  
        }  
        if (cleanSweep)  
            return false;  
    }  
    // Resort to locking all segments  
    for (int i = 0; i < segments.length; ++i)  
        segments[i].lock();  
    boolean found = false;  
    try {  
        for (int i = 0; i < segments.length; ++i) {  
            if (segments[i].containsValue(value)) {  
                found = true;  
                break;  
            }  
        }  
    } finally {  
        for (int i = 0; i < segments.length; ++i)  
            segments[i].unlock();  
    }  
    return found;  
}  

同样注意内层的第一个 for 循环,里面有语句 int c = segments[i].count; 但是 c 却从来没有被使用过,即使如此,编译器也不能做优化将这条语句去掉,因为存在对 volatile 变量 count 的读取,这条语句存在的唯一目的就是保证 segments[i].modCount 读取到几乎最新的值。关于 containsValue 方法的其它部分就不分析了,它和 size 方法差不多。跨段方法中还有一个 isEmpty()方法,其实现比 size()方法还要简单,也不介绍了

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3169 引用 • 8208 回帖 • 1 关注
  • segment
    1 引用

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...