Java并发系列之七:ConcurrentHashMap

回顾HashMap

既然说到HashMap了,那么我们就先来简单总结一下HashMap的重点。

1.基本结构

HashMap存储的是存在映射关系的键值对,存储在被称为哈希表(数组+链表/红黑树)的数据结构中。通过计算key的hashCode值来确定键值对在数组中的位置,假如产生碰撞,则使用链表或红黑树。

需要注意的是,key最好使用不可变类型的对象,否则当对象本身产生变化,重新计算key的hashcode时会与之前的不一样,导致查找错误。

由这一点可知,在存储键值对时,我们希望的情况是尽量避免碰撞。那么如何尽量避免碰撞?核心在于元素的分布策略和动态扩容。

2.分布策略

分布策略方面的优化主要为三个方向:

HashMap底层数组的长度始终保持为2的次幂

将哈希值的高位参与运算

通过与操作来等价取模操作

3.动态扩容

动态扩容方面,由于底层数组的长度始终为2的次幂,也就是说每次扩容,长度值都会扩大一倍,数组长度length的二进制表示在高位会多出1bit。

而扩容时,该length值将 会参与位于操作来确定元素所在数组中的新位置。所以,原数组中的元素所在位置要么保持不动,要么就是移动2次幂个位置。

以上三点都是关于HashMap本身设计特点,不在本文的主要讨论范围内。如果还不太熟悉,建议先了解HashMap的原理。

但是,HashMap美中不足的是:它不是线程安全的。主要体现在两个方面:

扩容时出现著名的环形链表异常,此问题在JDK1 .8版本被解决。

并发下脏读脏写

所以,程序员们就想要一种与HashMap功能同样强大,但又能保证读写线程安全的集合容器,这就是本文的主角——ConcurrentHashMap

HashTable

有的读者可能会有这样的疑惑:既然HashMap有线程安全问题,那我每次进行get/put操作时,都用锁进行控制不就好了?太对了,HashTable就是这么做的,可以看到它的源码里简单粗暴,给put/get操作都加上了sychronized。

public synchronized V get(object key) {
    Entry<?,?> tab[] = table;
    int hash = key.hashCode() ;
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) & e.key.equals(key)) {
            return (V)e.value;
        }
    }
    return null;
}
public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        throw new NullPointerException();
    }
    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings ("unchecked")
    Entry<K,V> entry = (Entry<K, V>)tab[index];
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) & entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }
    addEntry (hash, key, value, index);
    return null;
}

但这样会导致效率低下,在多线程环境下,当锁住map,进行读写操作时,其他想要操作的线程都会被阻塞。所以现在基本上都不再推荐使用HashTable。

有的读者可能又有疑惑了:ConcurrentHashMap难道就没有这个问题吗?它的内部应该也是用的锁吧?

ConcurrentHashMap

这就是本文要讲的重点,看看ConcurrentHashMap是如何在保证线程安全的情况下,并且做到高效的。

在JDK1.7和1.8中,ConcurrentHashMap的实现方式发生了很大的变化。有人可能觉得既然都已经更新了,那1.7就没有看的必要了。我认为读源码的目的是为了学习其中的思想,并尝试在今后的开发中进行运用。在1.7版本中,ConcurrentHashMap的分段锁思想比较经典,值得学习,本文主要从1.7版本说起。

HashTable之所以性能差是因为在每个方法上都加上了sychronized,这就相当于只用一把锁,锁住了整个数组资源。而ConcurrentHashMap用到了分段锁,每把锁只锁数组中的一段数据,这样就能大大减少锁的竞争。概念上很简单,那么具体是如何实现的呢?首先来看这么一张数据结构示意图:

 

可以看到,ConcurrentHashMap内部维护了一个segment数组,该数组的每个元素是HashEntry数组,看到HashEntry数组的这个结构是不是很熟悉,和HashMap中的哈希表如出一辙。

如果你读懂了这张图,基本上也就明白了ConcurrentHashMap是如何存储数据的,不过仅仅了解这些还不够,ConcurrentHashMap究竟通过哪些设计来保证其线程安全,我们需要进一步深挖。细节都藏在源码里。

这里再次声明一下,ConcurrentHashMap内部有很多和HashMap样的设计和技巧,一旦遇到,本文不再详细介绍。

源码

在JDK1.7版本中,ConcurrentHashMap的源码并不长,首先来看ConcurrentHashMap类的继承情况。

public class ConcurrentHashMap<K,V> extends AbstractMap<K, V>
    implements ConcurrentMap<K,V>,Serializable

继承情况

ConcurrentHashMap继承了AbstractMap抽象类,实现了ConcurrentMap接口。

AbstractMap类内部是一些Map通用方法的声明以及一些公共方法实现,本文不做深究。

ConcurrentMap接口中声明了四个方法,是对Map本身的增删改查,只不过要求实现类保证这些操作的线程安全,我们之后会看ConcurrentHashMap具体是如何实现的。

public interface ConcurrentMap<K, V> extends Map<K,V> {
    V putIfAbsent(K key, V value);
    boolean remove (object key, object value);
    boolean replace(K key, V oldValue, V newValue) ;
    V replace(K key, V value);
}

接下来就来看ConcurrentHashMap的内部实现。

静态变量

static final int DEFAULT_INITIAL_CAPACITY = 16;
static final float DEFAULT_LOAD_FACTOR = 0. 75f;
static final int DEFAULT_CONCURRENCY LEVEL = 16;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
static final int MAX_SEGMENTS = 1 << 16; // slightly conservative
static final int RETRIES_BEFORE_LOCK = 2;

以上各个静态变量都被final修饰了,而且都是基本数据类型,所以是作为常量来使用的。结合这张结构图,一期理解各个变量的含义。

 

DEFAULT_INITIAL_CAPACITY:HashEntry数组长度的初始值

DEFAULT_LOAD_FACTOR:加载因子,决定扩容时机,和HashMap里一样

DE FAULT_CONCURRENCY_LEVEL:并发等级(后续详细介绍)

MAXIMUM_CAPACITY:HashEntry数组长度的最大值,这里为2的30次方

MIN_SEGMENT_TABLE _CAPACITY:Segment数组最小长度,这里为2

MAX_SEGMENTS:Segment数组最大长度,这里为2的16次方

RETRIES_BEFORE_LOCK:重试次数(后续介绍在哪里用到)

属性

在没有看源码前,根据上述结构图,大致可以猜到最核心的属性应该有支持范型的Segment数组(其元素为HashEntry数组)。核心内部类应该就是Segment和HashEntry。在源码中也确实如此。

final int segmentMask;
final int segmentShift;

final Segment<K,V>[] segments; // Segment数组

// HashEntry数组保存的KV相关信息
transient Set<K> keySet;
transient Set<Map. Entry<K,V>> entrySet;
transient Collection<V> values;

除此之外,segmentMask和segmentShift这两个属性我们暂时还不明白它们的作用,后续用

到时会详细讲到。

内部类

HashEntry

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

    /**
     * Sets next field with volatile write semantics. (See above about use of 
     * putOrderedobject. )
     */
    final void setNext (HashEntry<K,V> n) {
        UNSAFE. putOrderedobject(this, nextOffset, n);
    }

    // Unsafe mechanics
    static final sun.misc.Unsafe UNSAFE;
    static final long nextOffset;

    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class k = HashEntry.class;
            nextOffset = UNSAFE.objectField0ffset(k. getDeclaredField("next");
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

HashEntry的结构不复杂,它拥有hash、key、value三个属性值,并且可以通过next引用来构建链表,整体上和HashMap中的内部类Node比较类似。但是不难发现26-35行有一段被static修饰的代码,这段代码是什么含义呢?

简单介绍一下Unsafe这个类,正如它的名字一样,Unsafe对象用于执行一些不安全的、较为底层的操作,比如直接访问系统资源。因为使用它的风险较高并且场景较少,所以我们在日常的业务代码中几乎看不到对Unsafe的使用,但是对于一些追求高效,并且有能力保证安全的下层组件来说,使用Unsafe是家常便饭。

第30行中objectField0ffset方法返回的是“指定成员属性在内存地址相对于此对象的内存地址的偏移量”,这句听上去比较拗口。在这里,使用该方法获取next属性的相对内存偏移量,然后方便在第10行中调用putOrderedObject来对next进行赋值,而putOrdered0bject下层是一个CAS调用。可以这么理解:这里直接使用Unsafe对象获取next的内存偏移量,是为了更方便地使用CAS对next进行赋值。如果这段话你不是很明白,忽略也不会影响后续理解。

整体上,HashEntry结构清晰,易于理解。下面我们来看看相对复杂一点的Segment。

Segment

继承关系

static final class Segment<K,V> extends ReentrantLock implements Serializable

Segment继承自ReentrantLock,这里分段锁的味道就体现出来了。每个Segment对象就是一把锁,一个Segment对象内部存在一个HashEntry数组,也就是说,HashEntry数组中的数据同步依赖同一把锁。不同HashEntry数组的读写互不干扰,这就形成了所谓的分段锁。

我们可以大胆猜想:假设Segment数组的长度为n,那么相较于Hashtable,理论上ConcurrentHashMap的性能就要提升n倍以上。有的读者可能会存在疑惑: ConcurrentHashMap用n把相互独立的锁替换Hashtable全局1把锁,那照理说性能提升最多也就是n倍,为什么要说n倍以上呢?

因为相较于Hashtable中使用的synchronized,ConcurrentHashMap对锁本身也做了优化。具体是怎么优化的,我们下文会讲到。

属性

static final int MAX_SCAN_RETRIES = Runtime.getRuntime().availableProcessors() > 1 ? 64 :1 ;
transient volatile HashEntry<K,V>[] table;
transient int count;
transient int modCount;
transient int threshold;
final float loadFactor;

MAX_ SCAN_ RETRIES:指定的重试次数。在多线程进行put操作时,只有一个线程能够成功获得锁进行写操作,那么此时其他线程也不必死等,可以通过多次tryLock进行重试,并做一些其他的工作,这就体现出了效率的提升,后面会讲到。

table:之前提到的HashEntry数组

count:HashEntry数组中元素个数

modCount:HashEntry数组修改次数

threshold:触发扩容的阈值

loadFactor:负载因子

上述属性也比较易于理解,基本和HashMap中同名属性的意义一样。

方法

final V put(K key, int hash, V value, boolean onlyIfAbsent) {}
private void rehash(HashEntry<K,V> node) {}
private HashEntry<K, V> scanAndLockForPut(K key, int hash, V value) {}
private void scanAndLock(0bject key, int hash) {}
final V remove(0bject key, int hash, object value) {}
final boolean replace(K key, int hash, V oldValue, V newValue) {}
final V replace(K key, int hash, V value) {}
final void clear() {}

方法基本上就是增删改查还有扩容,由于篇幅原因这里没有展示方法体内容,在讲解到具体方法时,再来看方法内部的具体逻辑。

但有两个方法名比较陌生:scanAndLockForPut和scanAndLock。 之前我们说到这样一个常见场景:当A线程正在修改HashEntry数组(属性名为table)的某个桶,此时B线程也想要修改这个桶,但是A线程持有了独占锁,所以B线程只能等待或重试,若只是干等或不断重试,可能会是一种浪费,所以有一种优化思路就是让B线程在重试的过程中抽空去预先完成一些后续将会用到的准备工作。

scanAndLockForPut和scanAndLock方法的逻辑就实现了这种优化,下文我们会按照自顶向下的流程详解这两个方法。

方法

至此为止,开胃菜吃完了,我们已经介绍了如下内容:

ConcurrentHashMap的属性(其中segmentMask和segmentShift还未介绍)

核心内部类HashEntry和Segment (其中Segment的方法体还未详解)

接下来就是正餐,看一看ConcurrentHashMap究竟如何实现线程安全的put操作,至于get、

replace、remove操作,相较于put更加简单,篇幅原因本文不再赘述。相信你如果能够理解put的设计后,其他都能通过举一反三的方式理解。

构造方法

首先看一看ConcurrentHashMap的构造方法,这将帮助你对它有一个更加直观的认识。

public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0) |I initialCapacity < 0 II concurrencyLevel <= 0) throw new IllegalArgumentException();
    if (concurrencyLevel > MAX_ SEGMENTS)
        concurrencyLevel = MAX_ SEGMENTS;
    // Find power-of-two sizes best matching arguments
    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;
    // create segments and segments [0]
    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); // ordered write of segments[0]
    this.segments = ss;
}

三个参数initialCapacity、loadFactor、 concurrencyLevel三个值的含义在开头介绍过,这三个值一般采用默认值16、0.75、16。

1.3-4行。对参数进行简单的校验,如果不满足条件则抛出异常

2.8-13行。出现了两个局部变量sshift、ssize。 ssize就是segment数组的长度,初始值为1,当ssize小于concurrencyLevel时,sshift自增1, ssize左移1位,相当于扩大两倍。也就是说,当concurrencyLevel为16时,ssize最终也为16,如果concurrencyLevel为17,那么ssize最终为32。为什么要这么设计呢?这是为了控制segment数组的长度始终为2的次幂,为什么要控制其为2的次幂?这是为了在计算元素索引时进行优化,和HashMap中的设计方式一样。

3.14-15行。 我们假设concurrencyLevel为16,此时,sshift为4, ssize为16。 那么segmentShift为28,segmentMask为15。ssize是segment数组长度并且总是2的次幂,segmentMask为ssize减1,二进制下为1111,这被称为掩码。在这里,segmentMask的二进制序列上每一位总是1。掩码用于与key的哈希值进行位与操作来代替取模,计算出索引值,以此定位key所在的桶。这个操作是不是很熟悉,在HashMap中也是用到了相同的设计。

4.16-23行。segment数组的长度确定了,接下俩需要确定segment数组的每个元素,即HashEntry数组的长度。变量cap即为计算后的HashEntry数组长度,相同地,cap也一定是2的次幂。

5.25-30行,确定了Segment与HashEntry的相关参数,接下来进行初始化。并且向Segment数组中加入了第一个元素。

万事俱备,只欠东风,准备阶段的内容都已经理解了的话。接下来我们就来看ConcurrentHashMap最经典的put操作。

Public put

public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    int hash = hash(key);
    int j = (hash >>> segmentShift) & segmentMask;
    if ((s = (Segment<K, V>) UNSAFE.get0bject  // nonvolatile; recheck
        (segments,(j << SSHIFT) + SBASE)) == null) // in ensureSegment
        s = ensureSegment(j) ;
    return s.put(key, hash, value, false) ;
}

public V putIfAbsent(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    int hash = hash(key);
    int j = (hash >>> segmentShift) & segmentMask;
    if ((s = (Segment<K,V>) UNSAFE.get0bject
            (segments,(j << SSHIFT) + SBASE)) == null)
        s = ensureSegment(j);
    return s.put(key,hash,value,true);
}

put操作主要有两个方法:put和putIfAbsent,不难发现除了最后一行,其他都一样。有的读者可能要说了:写的什么垃圾代码,懂不懂封装啊。(手动狗头)我理解这是为了屏蔽调用方的理解难度,这是闲话不表,继续看代码。主要就来讲put方法。

1.第5行。首先对key进行哈希,hash方法内部就是一系列的数学运算,细节这里就不介绍了。

2.第6行。接下来计算变量j的操作,首先移位,然后和掩码进行位与计算,其间的含义和HashMap中如出一辙。这里就不在赘述。

3.7-9行,通过索引j和ensureSegment方法来取出目标Segment对象。在介绍构造函数的篇幅中我们提过,构造函数中只为Segment数组第0个元素赋值。而ensureSegment内部会对 第j个元素是否存在进行判断,若不存在,则使用CAS进行初始化保证取出的s对象不为null。 ensureSegment内部的逻辑这里不深究,但是这种懒加载的思想值得学习。

Private put

该方法相对负复杂与核心。

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;
                    }
                    break;
                }
                e = e.next;
            } else {
                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;
}

先来看这几个形参:key、hash、value、onlylfAbsent。

前三个参数很易于理解,因为要插入新的HashEntry,那必定要构造该对象,这三个参数都是HashEntry的构造方法所需要的。最后一个onlylfAbsent指定插入条件,如果当前key已经存在,那么只有当onlylfAbsent为false时才会覆盖。

2-3行

首先就是一个三目运算符,终于见到了心心念念的tryLock()。因为插入是一个并发操作,这里通过ReentrantLock的性质来进行控制。如果当前线程获得了锁,那么将node置为null,等待后续的初始化;否则执行scanAndLockForPut方法。

这里有的读者可能会有疑问:为什么不直接调用ReentrantLock的lock方法,若第一时间没获得锁那就等待直到获取。我认为这样做也不是不行,但显而易见在这里等待的时间浪费了,作者Doug Lee提供了一种性能更高解决方法,也是精华所在。就是当某线程若没有第一时间获得锁,将会执行scanAndLockForPut方法,进行一些预处理工作,这样就减少了时间上的浪费,该方法我们下文会细讲。

4-22行

我们来看如果线程tryLock获得了锁的情况。首先计算index (length -1就是获得掩码)。我们知道,HashEntry数组table中每个元素都可能是HashEntry链表。entryAt通过index拿到HashEntry链表的头节点。接下来通过头节点去遍历链表,如果发现key已存在,则根据onlylfAbsent值判断是否应该覆盖value,然后退出;如果key不存在或头结点本身就是null,那么进入else块,执行插入新节点的逻辑。

23-35行

eles块中的逻辑比较丰富,需要仔细来看。首先判断node是否为null,这个判断有点略显奇怪,node只在最开始的三目运算中操作过,若当前线程抢到锁,那么node为null,而这里既然判断node是否为null,是不是也就是说若线程没抢到锁,执行scanAndLockForPut的结果,就是初始化node。这个可能性很大,我们暂不去证实,等到具体看scanAndLockForPut方法时再确定。

若node还是null,那么进行初始化,并插入链表,看到参数中有first,大致能猜到使用了头插法,看看HashEntry的构造方法中的逻辑,确认了一下果然。

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

接下来判断HashEntry数组是否需要扩容,如果不需要扩容,那么将node放入HashEntry数组的相应位置,这里需要注意的是,我们刚才说将node插入链表使用的是头插法,所以这里node已经成为了头结点。最终释放锁。

该方法因为一开始就已经用锁进行了控制,所以内部不会出现并发问题,理解上应该不难。

scanAndLockForPut

接下来就是上文一直提到的scanAndLockForPut方法,在进入put方法时,如果当前线程没有拿到锁,那么将会面临什么命运呢?接下去看。

private HashEntry<K, V> scanAndLockForPut(K key, int hash, V value) {
    HashEntry<K,V> first = entryForHash(this,hash);
    HashEntry<K,V> e = first;
    HashEntry<K,V> node = null;
    int retries = -1; // negative while locating node
    while (!tryLock()) {
        HashEntry<K,V> f; // to recheck first below
        if (retries < 0) {
            if (e == null) {
                if (node == null) // speculatively create node
                    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; // re-traverse if entry changed
            retries = -1;
        }
    }
    return node;
}

上文我们已经做出了一个大胆的猜想:scanAndLockForPut中,线程预创建了node,并插入了链表。

反应比较快的读者在这里可能会产生如下疑惑:如果scanAndLockForPut方法中根据key、value、hash预创建了node,但是后续返回的put方法中有逻辑会对当前key是否存在进行判断,如果已经存在,那么根本用不到这个预先创建的node,这样不是就浪费了,或者说根本没有预先创建的必要吗?

如果能产生这个疑问,那么说明已经完全理解了。但实际上的预创建的逻辑要更加高明一些,在创建过程中会对key是否存在进行判断。

这是一种预创建的思想,当一部分线程无事可干的时候,不要让它们干等,而是让它们去做一些可以预先完成的任务。以后在工作中遇到类似的场景时,完全可以借鉴这种思想。

在阅读该方法的逻辑时,我们需要知道其中逻辑都是处于并发状态下,因为获取锁的线程可能正在修改链表(增、删、改)。

6行

可以看到方法在第6行进入了一个while循环,当tryLock()为false,也就是说若当前线程未获得锁时,将会不断执行。

8-16行

retries值的初始值为-1,也就是说循环第一轮一定会进入这块逻辑。若e为null,也就是说链表的头结点为null,那么进入初始化node的逻辑;如果e不为null,那么遍历链表,一旦发现链表中存在相同key的node,就将retries置为0,表示不再进入这段可能初始化node的逻辑。

17-19行

若retries已经被置为>=0,说明因为存在相同key不需要创建node,或者node已经创建好了。这里就对retries开始自增,相当于自选,如果超过自选次数后还未获得锁,那么调用lock(),老老实实排队。

20-23行

如果当前retries&1== 0,这是个什么操作?也就是说retries每自增两次,将会出现一次retries&1 == 0。且此时出现了hash值一样的key的话,那么将会再次遍历链表检查是否需要创建node(因为也有可能目标key所在node已经被其他线程删除了)。以此往复,直到获取锁或retries超过阈值。

这个方法因为出现在了并发环境下,所以需要考虑的情况比较多,有兴趣的读者不妨画出流程图来仔细品一品。

扩容

最后我们再提一下扩容,ConcurrentHashMap中的扩容仅针对HashEntry数组,Segment数组在初始化后无法再扩容。

源码中我们也看到,在调用put操作时,会对是否需要rehash进行检查。扩容本身是很重要的知识点,但是由于HashEntry数组的扩容和HashMap中基本一样,所以就不赘述了。不同的是,HashEntry数组的扩容操作已经被外层put方法中获取的锁保护起来了,所以能保证线程安全。

本文讲解了JDK1.7版本的ConcurrentHashMap内部实现,相较于HashMap,它实现了线程安全的读写与扩容。相较于HashTable,它采用分段锁,通过并发等级这个参数来控制并发程度,提高了N倍的读写效率。ConcurrentHashMap的核心在于Segment和HashEntry的实现。此外,ConcurrentHashMap在进行put操作时,采用了一种“预先创建”的思想来进行优化,这是常常被人忽视,但是却很有趣的设计。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值