面试宝典:数据结构-ConcurrentHashMap

jdk1.7 分段锁

1、HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁

2、假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术

a、首先将数据分成一段一段的存储

b、然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问

c、有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁

d、在ConcurrentHashMap内部,段数组是final的,并且其成员变量实际上也是final的,但是,仅仅是将数组声明为final的并不保证数组成员也是final的,这需要实现上的保证。这可以确保不会出现死锁,因为获得锁的顺序是固定的。

3、ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成

4、Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色

5、HashEntry则用于存储键值对数据

6、一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构

7、一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。

jdk1.8 concurrentHashMap原理分析

1.8版本的ConcurrentHashMap的实现与1.7版本有很大的差别,放弃了段锁的概念,借鉴了HashMap的数据结构:数组+链表+红黑树

  • ConcurrentHashMap不接受nullkey和nullvalue

  • 数据结构:数组+链表+红黑树

  • 并发原理:cas乐观锁+synchronized锁

  • 加锁对象:数组每个位置的头节点

concurrentHashMap数据结构

ConcurrentHashMap的数据结构(数组+链表+红黑树),桶中的结构可能是链表,也可能是红黑树,红黑树是为了提高查找效率

构造函数

1、对于构造函数而言,会根据输入的initialCapacity的大小来确定一个最小的且大于等于initialCapacity大小的2的n次幂,如initialCapacity为15,则sizeCtl为16,若initialCapacity为16,则sizeCtl为16

2、若initialCapacity大小超过了允许的最大值,则sizeCtl为最大值

3、值得注意的是,构造函数中的concurrencyLevel参数已经在JDK1.8中的意义发生了很大的变化,其并不代表所允许的并发数 只是用来确定sizeCtl大小

4、在JDK1.8中的并发控制都是针对具体的桶而言,即有多少个桶就可以允许多少个并发数。

  • ConcurrentHashMap()

该构造函数用于创建一个带有默认初始容量 (16)、加载因子 (0.75) 和 concurrencyLevel (16) 的新的空映射

  • ConcurrentHashMap(int)

该构造函数用于创建一个带有指定初始容量、默认加载因子 (0.75) 和 concurrencyLevel (16) 的新的空映射

  • ConcurrentHashMap(Map)

该构造函数用于构造一个与给定映射具有相同映射关系的新映射

 public ConcurrentHashMap(Map m) {
        this.sizeCtl = DEFAULT_CAPACITY;
        // 将集合m的元素全部放入
        putAll(m);
    }
  • ConcurrentHashMap(int, float)
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
        this(initialCapacity, loadFactor, 1);
    }
  • ConcurrentHashMap(int, float, int)

putVal函数

V putVal(K key, V value, boolean onlyIfAbsent)

put函数底层调用了putVal进行数据的插入,对于putVal函数的流程大体如下。

① 判断存储的key、value是否为空,若为空,则抛出异常,否则,进入步骤②

② 计算key的hash值,随后进入无限循环,该无限循环可以确保成功插入数据,若table表为空或者长度为0,则初始化table表,否则,进入步骤③

③ 根据key的hash值取出table表中的结点元素,若取出的结点为空(该桶为空),则使用CAS将key、value、hash值生成的结点放入桶中。否则,进入步骤④

④ 若该结点的的hash值为MOVED,则对该桶中的结点进行转移,否则,进入步骤⑤

⑤ 对桶中的第一个结点(即table表中的结点)进行加锁,对该桶进行遍历,桶中的结点的hash值与key值与给定的hash值和key值相等,则根据标识选择是否进行更新操作(用给定的value值替换该结点的value值),若遍历完桶仍没有找到hash值与key值和指定的hash值与key值相等的结点,则直接新生一个结点并赋值为之前最后一个结点的下一个结点。进入步骤⑥

⑥ 若binCount值达到红黑树转化的阈值,则将桶中的结构转化为红黑树存储,最后,增加binCount的值

初始化table

Node[] initTable()

对于table的大小,会根据sizeCtl的值进行设置,如果没有设置szieCtl的值,那么默认生成的table大小为16,否则,会根据sizeCtl的大小设置table大小

获取table指定元素

static final  Node tabAt(Node[] tab, int i) {
        return (Node)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}

此函数返回table数组中下标为i的结点,可以看到是通过Unsafe对象通过反射获取的,getObjectVolatile的第二项参数为下标为i的偏移地址

比较并交换

static final  boolean casTabAt(Node[] tab, int i,Node c, Node v) {
   return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}

此函数用于比较table数组下标为i的结点是否为c,若为c,则用v交换操作。否则,不进行交换操作。

协助转移

Node[] helpTransfer(Node[] tab, Node f)

此函数用于在扩容时将table表中的结点转移到nextTable中

添加节点到红黑树

TreeNode putTreeVal(int h, K k, V v)

此函数用于将指定的hash、key、value值添加到红黑树中,若已经添加了,则返回null,否则返回该结点

转换成红黑树

void treeifyBin(Node[] tab, int index)

此函数用于将桶中的数据结构转化为红黑树,其中,值得注意的是,当table的长度未达到阈值时,会进行一次扩容操作,该操作会使得触发treeifyBin操作的某个桶中的所有元素进行一次重新分配,这样可以避免某个桶中的结点数量太大。

查询

V get(Object key)

get函数根据key的hash值来计算在哪个桶中,再遍历桶,查找元素,若找到则返回该结点,否则,返回null

删除

replaceNode

此函数对remove函数提供支持,remove函数底层是调用的replaceNode函数实现结点的删除

1、计算hash值

2、进入无限循环 确保删除成功

3、若table表为空或者表长度为0或者key所对应的桶为空 跳出循环

4、若桶中第一个结点的hash值为MOVED 则转移数据

5、删除操作

准备对桶中的第一个节点进行枷锁处理 但看看是否满足前提条件

a、判断桶中的第一个节点是否没有变化

a-1、节点hash值是否大于0

a-2、无限循环链表

a-3、判断 结点的hash值与指定的hash值相等,并且key也相等

a-4、进行替换或删除

b、判断是否为红黑树

b-1、 转换为treeNode结构

b-2、根节点不为空并且存在与指定hash和key相等的结点

b-3、进行替换或删除

你可能感兴趣的:(面试宝典:数据结构-ConcurrentHashMap)