currenthashmap扩容原理_Java并发编程笔记之ConcurrentHashMap原理探究

在多线程环境下,使用HashMap进行put操作时存在丢失数据的情况,为了避免这种bug的隐患,强烈建议使用ConcurrentHashMap代替HashMap。

HashTable是一个线程安全的类,它使用synchronized来锁住整张Hash表来实现线程安全,即每次锁住整张表让线程独占,相当于所有线程进行读写时都去竞争一把锁,导致效率非常低下。ConcurrentHashMap可以做到读取数据不加锁,并且其内部的结构可以让其在进行写操作的时候能够将锁的粒度保持地尽量地小,允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的Hashtable,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。

JDK1.7中的CouncurrentHashMap实现原理

ConcurrentHashMap 为了提高本身的并发能力,在内部采用了一个叫做 Segment 的结构,一个 Segment 其实就是一个类 Hash Table 的结构,Segment 内部维护了一个链表数组,我们用下面这一幅图来看下 ConcurrentHashMap 的内部结构,从下面的结构我们可以了解到,ConcurrentHashMap 定位一个元素的过程需要进行两次Hash操作,第一次 Hash 定位到 Segment,第二次 Hash 定位到元素所在的链表的头部,因此,这一种结构的带来的副作用是 Hash 的过程要比普通的 HashMap 要长,但是带来的好处是写操作的时候可以只对元素所在的 Segment 进行操作即可,不会影响到其他的 Segment,这样,在最理想的情况下,ConcurrentHashMap 可以最高同时支持 Segment 数量大小的写操作(刚好这些写操作都非常平均地分布在所有的 Segment上),所以,通过这一种结构,ConcurrentHashMap 的并发能力可以大大的提高。我们用下面这一幅图来看下ConcurrentHashMap的内部结构详情图,如下:

不难看出,ConcurrentHashMap采用了二次hash的方式,第一次hash将key映射到对应的segment,而第二次hash则是映射到segment的不同桶(bucket)中。

为什么要用二次hash,主要原因是为了构造分离锁,使得对于map的修改不会锁住整个容器,提高并发能力。当然,没有一种东西是绝对完美的,二次hash带来的问题是整个hash的过程比hashmap单次hash要长,所以,如果不是并发情形,不要使用concurrentHashmap。

JAVA7之前ConcurrentHashMap主要采用锁机制,在对某个Segment进行操作时,将该Segment锁定,不允许对其进行非查询操作,而在JAVA8之后采用CAS无锁算法,这种乐观操作在完成前进行判断,如果符合预期结果才给予执行,对并发操作提供良好的优化.

让我们先看JDK1.7的ConcurrentHashMap的原理分析

1.JDK1.7的ConcurrentHashMap

如上所示,是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表。

让我们看看Segment里面的成员变量,源码如下:

static final class Segment extends ReentrantLock implements Serializable {

transient volatile int count; //Segment中元素的数量

transient int modCount; //对table的大小造成影响的操作的数量(比如put或者remove操作)

transient int threshold; //阈值,Segment里面元素的数量超过这个值那么就会对Segment进行扩容

final float loadFactor; //负载因子,用于确定threshold

transient volatile HashEntry[] table; //链表数组,数组中的每一个元素代表了一个链表的头部

}

接着再看看HashEntry中的组成,源码如下:

/**

* ConcurrentHashMap列表Entry。注意,这不会作为用户可见的Map.Entry导出。

*/

static final class HashEntry {

final int hash;

final K key;

volatile V value;

volatile HashEntry next;

HashEntry(int hash, K key, V value, HashEntry next) {

this.hash = hash;

this.key = key;

this.value = value;

this.next = next;

}

/**

* 设置具有volatile写语义的next字段。

final void setNext(HashEntry n) {

UNSAFE.putOrderedObject(this, nextOffset, n);

}

// Unsafe mechanics

static final sun.misc.Unsafe UNSAFE;

//下一个HashEntry的偏移量

static final long nextOffset;

static {

try {

UNSAFE = sun.misc.Unsafe.getUnsafe();

Class k = HashEntry.class;

//获取HashEntry next在内存中的偏移量

nextOffset = UNSAFE.objectFieldOffset

(k.getDeclaredField("next"));

} catch (Exception e) {

throw new Error(e);

}

}

}

和 HashMap 非常类似,唯一的区别就是其中的核心数据如 value ,以及链表都是 volatile 修饰的,保证了获取时的可见性。

原理上来说:ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。

接着让我们继续看看JDK1.7中ConcurrentHashMap的成员变量和构造函数,源码如下:

// 默认初始容量

static final int DEFAULT_INITIAL_CAPACITY = 16;

// 默认加载因子

static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 默认segment层级

static final int DEFAULT_CONCURRENCY_LEVEL = 16;

// 最大容量

static final int MAXIMUM_CAPACITY = 1 << 30;

// segment最小容量

static final int MIN_SEGMENT_TABLE_CAPACITY = 2;

// 一个segment最大容量

static final int MAX_SEGMENTS = 1 << 16;

// 锁之前重试次数

static final int RETRIES_BEFORE_LOCK = 2;

public ConcurrentHashMap() {

this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);

}

public ConcurrentHashMap(int initialCapacity) {

this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);

}

public ConcurrentHashMap(int initialCapacity, float loadFactor) {

this(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL);

}

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;

// 找到两种大小的最匹配参数

int sshift = 0;

// segment数组的长度是由concurrentLevel计算来的,segment数组的长度是2的N次方,

// 默认concurrencyLevel = 16, 所以ssize在默认情况下也是16,此时 sshift = 4

// sshift相当于ssize从1向左移的次数

int ssize = 1;

while (ssize < concurrencyLevel) {

++sshift;

ssize <<= 1;

}

// 段偏移量,默认值情况下此时segmentShift = 28

this.segmentShift = 32 - sshift;

// 散列算法的掩码,默认值情况下segmentMask = 15

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 s0 =

new Segment(loadFactor, (int)(cap * loadFactor),

(HashEntry[])new HashEntry[cap]);

// 创建ssize长度的Segment数组

Segment[

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值