美文网首页
ConcurrentHashMap工作原理及实现

ConcurrentHashMap工作原理及实现

作者: 康俊1024 | 来源:发表于2019-04-04 08:39 被阅读0次

一、概述

HashMap是线程不安全的。HashTable是线程安全的。HashTable的线程安全是采用在每个方法来添加了synchronized关键字来修饰,即HashTable是针对整个table的锁定,这样就导致HashTable容器在竞争激烈的并发环境下表现出效率低下。

效率低下的原因说的更详细点:是因为所有访问HashTable的线程都必须竞争同一把锁。当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。(对象锁)

基于Hashtable的缺点,人们就开始思考,假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率呢??这就是我们的“锁分离”技术,这也是ConcurrentHashMap实现的基础。

ConcurrentHashMap的前世

ConcurrentHashMap使用的就是锁分段技术,ConcurrentHashMap由多个Segment组成(Segment下包含很多Node,也就是我们的键值对了),每个Segment都有把锁来实现线程安全,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。因此,关于ConcurrentHashMap就转化为了对Segment的研究。这是因为,ConcurrentHashMap的get、put操作是直接委托给Segment的get、put方法,这是JDK1.8之前的具体实现。

二、JDK1.8版本中ConcurrentHashMap介绍

前言

1、JDK1.8的ConcurrentHashMap中Segment虽保留,但已经简化属性,仅仅是为了兼容旧版本。
2、ConcurrentHashMap的底层与Java1.8的HashMap有相通之处,底层依然由“数组”+链表+红黑树来实现的,底层结构存放
的是TreeBin对象,而不是TreeNode对象;
3、ConcurrentHashMap实现中借用了较多的CAS算法,unsafe.compareAndSwapInt(this, valueOffset, expect, update);
CAS(Compare And Swap),意思是如果valueOffset位置包含的值与expect值相同,则更新valueOffset位置的值为update,
并返回true,否则不更新,返回false。
问题:ConcurrentHashMap既然借助了CAS来实现非阻塞的无锁实现线程安全,那么是不是就没有用锁了呢??
答案:还是使用了synchronized关键字进行同步了的,在操作hash值相同的链表的头结点还是会synchronized上锁,这样才能保证线程安全。
ConcurrentHashMap整个类的源码和HashMap的实现基本一模一样,当有修改操作时借助了synchronized来对table[i]进行锁定保证了线程安全以及使用了CAS来保证原子性操作,其它的基本一致,例如:ConcurrentHashMap的get(int key)方法的实现思路为:根据key的hash值找到其在table所对应的位置i,然后在table[i]位置所存储的链表(或者是树)进行查找是否有键为key的节点,如果有,则返回节点对应的value,否则返回null。

ConcurrentHashMap类中相关属性的介绍

0、private transient volatile int sizeCtl;//控制标识符
sizeCtl是控制标识符,不同的值表示不同的意义。
负数代表正在进行初始化或扩容操作 ,其中-1代表正在初始化 ,-N 表示有N-1个线程正在进行扩容操作
正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小,类似于扩容阈值。它的值始终是当前
ConcurrentHashMap容量的0.75倍,这与loadfactor是对应的。实际容量>=sizeCtl,则扩容。
1、transient volatile Node<K,V>[] table;是一个容器数组,第一次插入数据的时候初始化,大小是2的幂次方。这就是我们所说的
底层结构:”数组+链表(或树)”
2、private static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量
3、private static final int DEFAULT_CAPACITY = 16;
4、static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; // MAX_VALUE=2^31-1=2147483647
5、private static finalint DEFAULT_CONCURRENCY_LEVEL = 16;
6、private static final float LOAD_FACTOR = 0.75f;
7、static final int TREEIFY_THRESHOLD = 8; // 链表转树的阀值,如果table[i]下面的链表长度大于8时就转化为数
8、static final int UNTREEIFY_THRESHOLD = 6; //树转链表的阀值,小于等于6是转为链表,仅在扩容tranfer时才可能树转链表
9、static final int MIN_TREEIFY_CAPACITY = 64;
10、private static final int MIN_TRANSFER_STRIDE = 16;
11、private static int RESIZE_STAMP_BITS = 16;
12、private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1; // help resize的最大线程数
13、private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
14、static final int MOVED = -1; // hash for forwarding nodes(forwarding nodes的hash值)、标示位
15、static final int TREEBIN = -2; // hash for roots of trees(树根节点的hash值)
16、static final int RESERVED = -3; // hash for transient reservations(ReservationNode的hash值)

ConcurrentHashMap的构造函数

构造函数主要干了两件事:
1、参数的有效性检查
2、table初始化的长度(如果不指定默认情况下为16)。这里要说一个参数:concurrencyLevel,表示能够同时更新ConccurentHashMap且不产生锁竞争的最大线程数。默认值为16,(即允许16个线程并发可能不会产生竞争)。为了保证并发的性能,我们要很好的估计出concurrencyLevel值,不然要么竞争相当厉害,从而导致线程试图写入当前锁定的段时阻塞。

ConcurrentHashMap类中相关节点类:Node/TreeNode/TreeBin

1、Node类
Node类是table数组中的存储元素,即一个Node对象就代表一个键值对(key,value)存储在table中。
Node类是没有提供修改入口的(唯一的setValue方法抛异常),因此只能用于只读遍历。
这个类可以与HashMap中的Node类的具体代码进行比较,发现在具体的实现上,有一定的细微的区别。
2、TreeNode
和HashMap相比,这里的TreeNode相当简洁;ConcurrentHashMap链表转树时,并不会直接转,正如注释(Nodes for use in
TreeBins)所说,只是把这些节点包装成TreeNode放到TreeBin中, 再由TreeBin来转化红黑树。
3、TreeBin
TreeBin用于封装维护TreeNode,包含putTreeVal、lookRoot、UNlookRoot、remove、balanceInsetion、balanceDeletion等方法,当链表转树时,用于封装TreeNode,也就是说,ConcurrentHashMap的红黑树存放的是TreeBin,而不是treeNode。

相关文章

网友评论

      本文标题:ConcurrentHashMap工作原理及实现

      本文链接:https://www.haomeiwen.com/subject/rkxxyqtx.html