本书来自《Java并发编程的艺术》
ConcurrentHashMap是线程安全且高效的HashMap。
本文我们一起来研究一下该容器是如何在保证线程安全的同时又能保证高效的操作。
为什么要使用ConcurrentHashMap?
在并发编程中使用HashMap可能导致死循环。而使用线程安全的HasTable效率又非常低下。
1、线程不安全的HashMap
在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。
HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,
Entry的next节点永远不为空,就会产生死循环获取Entry。
2、效率低下的HasTable
当一个线程访问HasTable的同步方法,其他线程也访问HasTable的同步方法时,会进入阻塞或轮询状态。
3、ConcurrentHashMap的锁分段技术可有效提升并发访问率
HasTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HasTable的线程都必须竞争同一把锁,
假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,
从而可以有效提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术。首先将数据分成一段一段地存储,然后给每一段数据配
一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁(ReentrantLock),在ConcurrentHashMap里
扮演锁的角色;HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种
数组和链表结构。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,
当对HashEntry数组的数据进行修改时,必须首先获得与它对应的Segment锁。
JDK1.8的ConcurrentHashMap怎么实现线程安全的
JDK1.8放弃了锁分段的做法,采用CAS和synchronized方式处理并发。以put操作为例,CAS方式确定key的数组下标,synchronized保证链表节点的同步效果。
jdk1.8ConcurrentHashMap是数组+链表,或者数组+红黑树结构,并发控制使用Synchronized关键字和CAS操作。
ConcurrentHashMap
<jdk1.7> :使用 Segment数组 + HashEntry数组 + 链表
<jdk1.8> :使用 Node数组+链表+ 红黑树
外部类的基本属性
volatile Node<K,V>[] table; // Node数组用于存放链表或者树的头结点
static final int TREEIFY_THRESHOLD = 8; // 链表转红黑树的阈值 > 8 时
static final int UNTREEIFY_THRESHOLD = 6; // 红黑树转链表的阈值 <= 6 时
static final int TREEBIN = -2; // 树根节点的hash值
static final float LOAD_FACTOR = 0.75f;// 负载因子
static final int DEFAULT_CAPACITY = 16; // 默认大小为16
内部类
class Node<K,V> implements Map.Entry<K,V> {
int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
}
jdk1.8中虽然不在使用分段锁,但是仍然有Segment这个类,但是没有实际作用
- ConcurrentHashMap的key和Value都不能为null
CAS:在判断数组中当前位置为null的时候,使用CAS来把这个新的Node写入数组中对应的位置
synchronized :当数组中的指定位置不为空时,通过加锁来添加这个节点进入数组(链表<8)或者是红黑树(链表>=8)