ConcurrentHashMap
HashMap多线程死循环Demo
在之前HashMap的文章中提到,HashMap是非线程安全的。
在多线程环境下使用HashMap的put操作会引发死循环,所以在并发/多线程情况下不能使用HashMap。
我们看下面的这个Demo
package com.hashmap.hashmap_1;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
public class HashMapThread extends Thread {
private static AtomicInteger ai = new AtomicInteger(0);// 以原子方式增加的计数器
private static Map<Integer, Integer> map = new HashMap<Integer, Integer>();
@Override
public void run() {
while (ai.get() < 10000000) {
map.put(ai.get(), ai.get());
ai.incrementAndGet();// 原子方式int值+1
}
}
}
package com.hashmap.hashmap_1;
public class TestMain {
public static void main(String[] args) {
HashMapThread t0 = new HashMapThread();
HashMapThread t1 = new HashMapThread();
HashMapThread t2 = new HashMapThread();
HashMapThread t3 = new HashMapThread();
HashMapThread t4 = new HashMapThread();
HashMapThread t5 = new HashMapThread();
HashMapThread t6 = new HashMapThread();
HashMapThread t7 = new HashMapThread();
t0.start();
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
t6.start();
t7.start();
}
}
输出结果:
程序一直在死循环,很长时间了..
这个要多执行几次,有几次是下标越界,还有遇到过内存溢出的情况,多执行几次,就能出现死循环..
使用jstack查看线程状态
发现是HashMap在put操作、扩容时,引发死循环。
因为多线程会导致HashMap的Entry链表形成了环形数据结构,从而引发死循环。
ConcurrentHashMap的锁分段技术在并发场景下,为什么效率好?
原书的描述:
HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁。
ConcurrentHashMap首先将数据分成一段段的存储,然后给每一段数据配上一把锁,当一个线程占用锁访问其中
一段数据时,其他段的数据也能被其他线程访问
HashTable和ConcurrentHashMap的对比
HashTable为什么是线程安全的?
我们看下HashTable对外方法的源码:
public synchronized int size() {
return count;
}
public synchronized boolean isEmpty() {
return count == 0;
}
public synchronized Enumeration<K> keys() {
return this.<K>getEnumeration(KEYS);
}
public synchronized V get(Object key) {
Entry tab[] = table;
// 计算key的hash值
int hash = key.hashCode();
// 计算hash值在table数组中的下标
int index = (hash & 0x7FFFFFFF) % tab.length;
// 从链表Entry中取出value值
for (Entry<K, V> e = tab[index]; e != null; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return e.value;
}
}
return null;
}
public synchronized V put(K key, V value) {
// Make sure the value is not null
// 确保value不为空
if (value == null) {
throw new NullPointerException();
}
/**
* 处理key在table数组中是不重复的<br>
* 处理过程:<br>
* 1.计算key的hash值,确认在table数组中的索引位置<br>
* 2.迭代index索引位置,如果该位置处的链表中存在一个一样的key,则替换其value,返回旧值<br>
*/
// Makes sure the key is not already in the hashtable.
Entry tab[] = table;
int hash = key.hashCode();// 计算key的hash值
int index = (hash & 0x7FFFFFFF) % tab.length;// 确认该key的索引位置
// 迭代,寻找该key,替换
for (Entry<K, V> e = tab[index]; e != null; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
V old = e.value;
e.value = value;
return old;
}
}
modCount++;
// 如果容器中的元素数量达到阀值,则进行扩容操作
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
// 在索引位置处插入一个新的节点
Entry<K, V> e = tab[index];
tab[index] = new Entry<>(hash, key, value, e);
// 容器中元素+1
count++;
return null;
}
public synchronized void putAll(Map<? extends K, ? extends V> t) {
for (Map.Entry<? extends K, ? extends V> e : t.entrySet())
put(e.getKey(), e.getValue());
}
.......
通过上面HashTable对外提供的方法(public方法)源码,发现什么没有??
所有方法的源码都加上了synchronized锁!
也就是说,在操作HashTable对象容器时,它给整个容器加了一个锁。
当线程1获取锁,操作HashTable对象容器时,其他的线程只能等着,等线程1干完了,才能获取锁。
看过HashMap源码的童鞋肯定知道,HashMap的底层实现是数组+链表;
ConcurrentHashMap将数组分段,分段存储数据,给每个分段加上锁;
当线程1进入ConcurrentHashMap对象容器中操作数据时,先找到对应的分段数据,然后将此分段数据加锁,接着操作数据。
其他线程进入ConcurrentHashMap对象容器中操作数据时,只要不是和线程1一样的分段数据,都不会有影响。
说简单些,ConcurrentHashMap就是降低锁的颗粒度。
(网上找的图片,感谢原作者)
ConcurrentHashMap内部存储结构
(感谢原作者图片)
简单说下:
Segments:就是整个Hash表
Segment:段(分段数据)
table:一个table就相当于一个HashTable。
(其实我觉得原博这样说会有歧义,HahsMap底层是由数组+链表组成,我觉得这个table就是数组中几个相连元素组成的一个数据段,
就比如HashMap的table[]数组的长度是16,ConcurrentHashMap的table中就是HahsMap中的table[0]+table[1]+table[2]+table[3]组成的一个数据段)
HashEntry
HashEntry是最底层存储元素的,HashEntry自己维护了链表
ConcurrentHashMap中的HashEntry比HashMap中的Entry要简洁
参数定义和HashMap中的Entry一样。
static final class HashEntry<K, V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K, V> next;
HashEntry(int hash, K key, V value, HashEntry<K, V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
/**
* Sets next field with volatile write semantics. (See above about use
* of putOrderedObject.)
*/
final void setNext(HashEntry<K, V> n) {
UNSAFE.putOrderedObject(this, nextOffset, n);
}
// Unsafe mechanics
static final sun.misc.Unsafe UNSAFE;
static final long nextOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class k = HashEntry.class;
nextOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("next"));
} catch (Exception e) {
throw new Error(e);
}
}
}
Segment继承了ReentrantLock(重入锁),使得Segment可以充当锁的角色。
每个Segment对象守护其中包含的多个桶(多个Entry链表)。
我们看下Segment的源码:
static final class Segment<K, V> extends ReentrantLock implements Serializable {
static final int MAX_SCAN_RETRIES = Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
//table是HashEntry对象组成的数组
//table数组的成员代表一个HashEntry链表
//每个table守护整个ConcurrentHashMap中的一部分
//如果concurrencyLevel为16,那么table则守护ConcurrentHashMap总数据的1/16
transient volatile HashEntry<K, V>[] table;
//在本segment范围内,包含HashEntry元素的个数
transient int count;
//table被更新的次数
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;
}
}
count:该变量是一个计数器,它表示每个Segment对象管理的table数组(若干个HashEntry组成的链表)包含HashEntry对象的个数。
每一个Segment对象内部都有一个count属性来代表自己Segment中包含的HashEntry对象的总数。
之所以在每个Segment对象中包含一个计数器,而不是在ConcurrentHashMap中使用全局的计数器,是为了避免出现"热点域"而影响
ConcurrentHashMap的并发性(来自原博,个人保留意见)
ConcurrentHashMap构造方法
ConcurrentHashMap():
创建一个带有默认初始容量 (16)、加载因子 (0.75) 和 concurrencyLevel (16) 的新的空映射。
ConcurrentHashMap(int initialCapacity):
创建一个带有指定初始容量、默认加载因子 (0.75) 和 concurrencyLevel (16) 的新的空映射。
ConcurrentHashMap(int initialCapacity, float loadFactor):
创建一个带有指定初始容量、加载因子和默认 concurrencyLevel (16) 的新的空映射。
ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)
创建一个带有指定初始容量、加载因子和并发级别的新的空映射。
ConcurrentHashMap(Map<? extends K, ? extends V> m)
构造一个与给定映射具有相同映射关系的新映射。参数默认
关于concurrencyLevel参数,源码中的注释:
The default concurrency level for this table, used when not otherwise specified in a constructor.
翻译过来的意思:此表的默认并发级别,在构造函数中未另外指定时使用
一开始没搞明白,网上查阅了下,有的博主说这个参数代表ConcurrentHashMap内部的Segment数组的数量,
然后对照着源码,恩,原博说的不错╮(╯▽╰)╭。
我们来看下默认的构造方法源码:
public ConcurrentHashMap(int initialCapacity, floatloadFactor, int concurrencyLevel) {
// 参数校验,参数异常直接抛出异常 if (!(loadFactor > 0) || initialCapacity < 0 ||concurrencyLevel <= 0) throw new IllegalArgumentException();
// 如果concurrencyLevel超过最大阀值,就使用最大阀值参数 if (concurrencyLevel > MAX_SEGMENTS) concurrencyLevel = MAX_SEGMENTS; // Find power-of-two sizes best matching arguments
int sshift = 0; int ssize = 1; while (ssize <concurrencyLevel) { ++sshift; ssize <<= 1; }
this.segmentShift = 32 - sshift; 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<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; } |
从上面这个构造方法源码可以看出,Segment数组的长度ssize是通过concurrencyLevel计算出来的。
concurrencyLevel一经指定,不可改变,后续如果ConcurrentHashMap的元素增加导致需要扩容,ConcurrentHashMap不会增加Segment数组的数量。
只会增加Segment中链表数组的容量大小,这样的好处就是扩容不需要对整个ConcurrentHashMap做rehash,
而只需要对Segment里面的元素做一次rehash就好。
整个ConcurrentHashMap初始化过程大致如下:
- 先根据concurrencyLevel计算出ssize,然后根据ssize创建Segment数组。
- Segment数组的数量(长度)是不大于concurrencyLevel的,并且是2的幂次;也就是说segment数组的数量(长度)永远是2的幂次,
这样方便移位操作计算hash,加快hash过程。
- 根据initalCapacity和loadFactor确定每个segment容量的大小,每一个segment的容量也是2的指数,同样为了加快hash的过程。
简单说下concurrencyLevel和ssize的关系:
Segment数组的大小ssize是由concurrentLevel来决定的,但是却不一定concurrentLevel;
ssize一定是大于或等于concurrentLevel的最小的2的幂次。
举个栗子:
默认情况下,concurrentLevel是16,则ssize为16。
若concurrentLevel是14,ssize为16。
若concurrentLevel是17,ssize为32。
为什么Segment的数组大小一定是2的幂次呢?
主要是便于通过按位与的散列算法来定位Segment的index。
参数initialCpacity是ConcurrentHashMap的初始容量;
loadfactor是每个segment的负载因子,在构造方法中通过这两个参数来初始化每个segment。
具体方法暂时放着..
参考资料:
http://blog.csdn.net/dingji_ping/article/details/51005799#