ConcurrentHashMap
ConsurrntHashMap是线程安全且高效的HashMap。
- 存在包:(1.7)
java.util.concurrent包; - 继承关系:
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
implements ConcurrentMap<K, V>, Serializable
继承AbstractMap,实现了ConcurrentMap接口和序列化接口;该接口下的特有方法:
V putIfAbsent(K key, V value)
如果指定键已经不再与某个值相关联,则将它与给定值关联。
boolean remove(Object key, Object value)
只有目前将键的条目映射到给定值时,才移除该键的条目。
V replace(K key, V value)
只有目前将键的条目映射到某一值时,才替换该键的条目。
boolean replace(K key, V oldValue, V newValue)
只有目前将键的条目映射到给定值时,才替换该键的条目。
- 基本属性:
//默认初始化大小-----》hashentry的table属性
static final int DEFAULT_INITIAL_CAPACITY = 16;
//默认加载因子----》HashEntry
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//默认并发度------》segment数组大小
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
//最大容量 -----》segment数组的最大容量 ->tab.length<MAXIMUM_CAPACITY
static final int MAXIMUM_CAPACITY = 1 << 30;
//HashEntry数组的大小
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;//最小的桶的数组的容量
//最大并发度 == segment数组的最大值
static final int MAX_SEGMENTS = 1 << 16; // slightly conservative最大的桶的个数
//锁重试次数
static final int RETRIES_BEFORE_LOCK = 2;
主要作用于keyhash过程
final int segmentMask; //segment的掩码,用来对segment进行定位,判断哪个segment
final int segmentShift; //segment的偏移,segment中的索引
final Segment<K,V>[] segments;//segments数组,类似于整个ConcurrentHashMap的外层数据结构</k,v>
集合:迭代器中以键形式遍历
transient Set<K> keySet;
集合:迭代器中以键值对形式遍历时使用
transient Set<Map.Entry<K,V>> entrySet;
集合:迭代器以值得形式遍历时使用
transient Collection<V> values;
- 底层数据结构:hash table (数组+数组+链表),segment+table+ HashEntry,通过hash函数
哈希到相应的位置。
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。
1.Segment继承一种可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;
2.HashEntry则用于存储键值对数据。
3.一个ConcurrentHashMap里包含一个Segment数组。
4.Segment的结构和HashMap类似,是一种数组和链表结构。
5.一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得与它对应的Segment锁。
/**
* The segments, each of which is a specialized hash table.
*/
final Segment<K,V>[] segments; //segment数组
/**
* The per-segment table. Elements are accessed via
* entryAt/setEntryAt providing volatile semantics.
*/
transient volatile HashEntry<K,V>[] table;//segment下的table数组
/**
* ConcurrentHashMap list entry. Note that this is never exported
* out as a user-visible Map.Entry.链表
*/
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
}//(部分结构)
如图所示底层存储结构:
- 构造函数:
//创建一个带有指定初始容量、加载因子和并发级别的新的空映射。(并发度:同一时刻线程的个数)
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;
// Find power-of-two sizes best matching arguments
int sshift = 0;
int ssize = 1;
//大于concurrencyLevel最小的2的幂的值
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
//利用segmentShift和segmentMask可以通过key的hash值与这个值做&运算确定Segment索引
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;//检查给的容量值是否大于允许的最大容量值
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)//计算每个Segment平均应该放置多少个元素,向上取整
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
// create segments and segments[0],创建一个segment实例,并作为segment数组的一个元素
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;
}
//创建一个带有指定初始容量、默认加载因子 (0.75) 和 concurrencyLevel (16) 的新的空映射。
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL);
}
//创建一个带有指定初始容量、默认加载因子 (0.75) 和 concurrencyLevel (16) 的新的空映射。
public ConcurrentHashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
//创建一个带有默认初始容量 (16)、加载因子 (0.75) 和 concurrencyLevel (16) 的新的空映射。
/**
* Creates a new, empty map with a default initial capacity (16),
* load factor (0.75) and concurrencyLevel (16).
*/
public ConcurrentHashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
//创造一个新的map与给定的map相同。这个映射被创造具有给定的map的1.5倍的大小,默认的加载因子0.75和并发度16.
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY),
DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
putAll(m);
}
- 方法简述:
void clear(): 从该映射中移除所有映射关系,即删除所有键值对。
boolean containsKey(Object key):判断指定对象是否为此表中的键。
boolean containsValue(Object value):如果此映射将一个或多个键映射到指定值,则返回 true。
Enumeration<V> elements():返回此表中值的枚举。
Enumeration<K> keys():返回此表中键的枚举。
Set<Map.Entry<K,V>> entrySet():返回此映射所包含的映射关系的 Set 视图。
Set<K> keySet():返回此映射中包含的键的 Set 视图。
Collection<V> values(): 返回此映射中包含的值的 Collection 视图。
void putAll(Map<? extends K,? extends V> m):将指定映射中所有映射关系复制到此映射中。
V putIfAbsent(K key, V value):如果指定键已经不再与某个值相关联,则将它与给定值关联。
V remove(Object key):从此映射中移除键(及其相应的值)。
boolean remove(Object key, Object value):只有目前将键的条目映射到给定值时,才移除该键的条目。
V replace(K key, V value):只有目前将键的条目映射到某一值时,才替换该键的条目。
boolean replace(K key, V oldValue, V newValue):只有目前将键的条目映射到给定值时,才替换该键的条目。
boolean isEmpty():如果此映射不包含键-值映射关系,是否为空,则返回 true。
V get(Object key):返回指定键所映射到的值,如果此映射不包含该键的映射关系,则返回 null。
V put(K key, V value):将指定键映射到此表中的指定值。
int size():返回此映射中的键-值映射关系数,键值对个数。
ConcurrentHashMap通过什么来保证线程安全??
ConcurrentHashMap中的主要重点就在于通过一个子类Segment继承ReentrantLock实现其线程安全。
实现原理:
ConcurrentHashMap将数据分别放到多个Segment中,默认16个,每一个Segment中又包含了多个HashEntry列表数组table,table中每个元素是以链表的形式存在的。
锁分段技术:将锁分解技术进一步扩展为对一组独立对象上的锁进行分解,这种情况被称为锁分段。容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率,这就ConcurrentHashMap所使用的锁分段技术。首先将数据分成一段一段地存储(segment),然后给每一段数据配一把锁(ReentrantLock),当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
/**
* Segments are specialized versions of hash tables. This
* subclasses from ReentrantLock opportunistically, just to
* simplify some locking and avoid separate construction.
*/
//继承ReentrantLock,说明每一个Segment都是一个锁
static final class Segment<K,V> extends ReentrantLock implements Serializable {
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;
Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
this.loadFactor = lf;
this.threshold = threshold;
this.table = tab;
}
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;//保证每次获取到新的value
volatile HashEntry<K,V> next;//保证每次获取到新的next结点
HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
- 增删改查(CRUD)方法:
(1)put()方法:put方法里需要对共享变量进行写入操作,所以为了线程安全,在操作共享变量时必须加锁。
put方法首先定位到Segment,然后在Segment里进行插入操作。
插入操作需要经历两个步骤,
第一步判断是否需要对Segment里的HashEntry数组进行扩容;
在插入元素前会先判断Segment里的HashEntry数组是否超过容量(threshold),如果超过阈值,则对数组进行扩容。
扩容方式:在扩容的时候,首先会创建一个容量是原来容量两倍的数组,然后将原数组里的元素进行重哈希后插入到新的数组里。为了高效,ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment进行扩容。
第二步定位添加元素的位置,然后将其放在HashEntry数组里。重哈希中两个for循环,第一个是减少HashEntry重哈希的次数,第二次是针对其他结点的元素逐个定位到新数组的指定位置;
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException(); //ConcurrentHashMap中值和键都不能为null
int hash = hash(key); //求出key的hash值
int j = (hash >>> segmentShift) & segmentMask; //求key在segments数组中的哪一个segment中
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j); //使用unsafe操作取出该segment
return s.put(key, hash, value, false); //调用segment中的put方法
}
//segment.put() 将一个HashEntry放入到该Segment中,使用自旋机制,减少了加锁的可能性
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value); //尝试性加锁,如果失败调用该方法,该操作持续查找key对应的节点链中是否已存在该节点,如果没有找到已存在的节点,则预创建一个新节点,并且尝试n次,直到尝试次数超出限制,才真正进入等待状态,即所谓的自旋等待。
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash; //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))) {
//相等的情况下 onlyIfAbsent:false:当key相等时,返回旧value值,并将旧value替换新value值,true:仅将旧value值返回
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else { 当前索引位置没有节点,或者是没有找到key相等的节点
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;
}
//重哈希
private void rehash(HashEntry<K,V> node) {
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
int newCapacity = oldCapacity << 1;
//按照2*table.length大小进行扩容
threshold = (int)(newCapacity * loadFactor);
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
int sizeMask = newCapacity - 1;
//哈希需要用到的参数
for (int i = 0; i < oldCapacity ; i++) {
//对旧table的所有的索引位置链表进行遍历
HashEntry<K,V> e = oldTable[i];
if (e != null) {
HashEntry<K,V> next = e.next;
//头结点不会空,获取next的节点
int idx = e.hash & sizeMask;
//头结点重新哈希的位置
if (next == null) // Single node on list
//该索引位置只有一个节点,直接给新表的对应idx位置
newTable[idx] = e;
else { // Reuse consecutive sequence at same slot
HashEntry<K,V> lastRun = e;
int lastIdx = idx;
for (HashEntry<K,V> last = next;
last != null;
last = last.next) {
int k = last.hash & sizeMask;
if (k != lastIdx) {
lastIdx = k;
lastRun = last;
}
}
newTable[lastIdx] = lastRun;
// Clone remaining nodes
for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
V v = p.value;
int h = p.hash;
int k = h & sizeMask;
HashEntry<K,V> n = newTable[k];
newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
}
}
}
}
int nodeIndex = node.hash & sizeMask; // add the new node,创建新节点
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
table = newTable;
}
(2)get方法:根据key值找到相应的value。先经过一次哈希,然后使用这个哈希值通过哈希算法定位到Segment,再通过哈希算法定位到元素。
过程中不需要加锁,使用的都是共享变量,由volatile修饰能够在线程之间保持可见性,多线程同时读,但是只能被单线程写。
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key); //找出key相应的segment的位置
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE); //找出相应的HashEntry,从头开始遍历
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k))) //判断key是否相等,equals,成功则返回对应的value
return e.value;
}
}
return null;
}
注意点:
读操作对加锁的需求:
(1).HashEntry 中的 key,hash,next 都声明为 final 型:
在代码清单HashEntry 类的定义中我们可以看到,HashEntry 中的 key,hash,next 都声明为 final型。
这意味着,不能把节点添加到链接的中间和尾部,也不能在链接的中间和尾部删除节点。
这个特性可以保证:在访问某个节点时,这个节点之后的链接不会被改变。
这个特性可以大大降低处理链表时的复杂性;(重哈希时在第一次for循环时体现)
(2).HashEntry 类的 value 域被声明为 Volatile 型:
同时,HashEntry 类的 value 域被声明为 Volatile 型,
Java 的内存模型可以保证:某个写线程对 value 域的写入马上可以被后续的某个读线程“看”到。
在 ConcurrentHashMap 中,不允许用 null 作为键和值,当读线程读到某个 HashEntry 的 value 域的值为 null 时,便知道产生了冲突(发生了重排序现象),需要加锁后重新读入这个 value 值。
这些特性互相配合,使得读线程即使在不加锁状态下,也能正确访问 ConcurrentHashMap;
由于对 Volatile 变量的写入操作将与随后对这个变量的读操作进行同步。
当一个写线程修改了某个 HashEntry 的 value 域后,另一个读线程读这个值域,Java 内存模型能够保证读线程读取的一定是更新后的值。所以,写线程对链表的非结构性修改能够被后续不加锁的读线程看到;
(3) 在size()、remove()方法中,使用全局加锁,对整个segments数组进行加锁,这个也是同时要注意的点;
- HashTable和concurrentHashMap线程安全保证机制是否一样?
不一样,
Hashtable对方法前加Sychronized,作用于普通方法,加锁是加到对象,同一时刻只有一个线程处理,保证线程安全;
ConcurrentHashMap用一个子类Segment来继承ReentrantLock加锁,分段锁,多线程下并发效率高,保证线程安全;
HashMap、HashTable和ConcurrentHashMap区别是什么???
(1) 线程不安全的HashMap
在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所
以在并发情况下不能使用HashMap。HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry。
扩容方式:2* table.length扩容,之前的数据重哈希到新的位置。
底层数据结构:哈希表(数组+链表)。
线程不安全。
(2)效率低下的HashTable
HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。
因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞或轮询状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。
同一时刻只有一个线程处理。
扩容方式:2 * table.length + 1 扩容。
底层数据结构:哈希表(数组+链表)。
(3)ConcurrentHashMap的锁分段技术可有效提升并发访问率
HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁。
对于ConcurrentHashMap,假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术。首先将数据分成一段一段地存
储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数
据也能被其他线程访问。
扩容方式:扩容:segment数组大小不变,segment数组元素下的table数组进行扩容。
底层数据结构:数组+ 数组+ 链表。
线程安全。
- ConcurrentHashMap的高并发性主要来自于三个方面:
(1)用分离锁(Segment锁)实现多个线程间的更深层次的共享访问;
(2)用 HashEntery 对象的不变性来降低执行读操作的线程在遍历链表期间对加锁的需求;
(3)通过对同一个 Volatile 变量的写 / 读访问,协调不同线程间读 / 写操作的内存可见性;