众所周知,哈希表是非常高效,复杂度为O(1)的数据结构,在Java开发中,我们最常见到最频繁使用的就是HashMap和HashTable,但是在线程竞争激烈的并发场景中使用都不够合理。
HashMap :先说HashMap,HashMap是线程不安全的,在并发环境下,可能会形成环状链表(扩容时可能造成,具体原因自行百度google或查看源码分析),导致get操作时,cpu空转,利用率可能达到100%,所以,在并发环境中使用HashMap是非常危险的。
HashTable : HashTable和HashMap的实现原理几乎一样,差别无非是1.HashTable不允许key和value为null;2.HashTable是线程安全的。但是HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。一把大锁锁所有
ConcurrentHashMap,数据结构如图,数组+数组+链表,
使用了分段锁技术将数据分成一段一段来存储,每一段都有一把ReentranLock(重入锁)锁,,当,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问
//ConcurrentHashMap继承AbstractMap
实现ConcurrentMap接口 既有map属性和多线程的属性
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
implements ConcurrentMap<K, V>, Serializable {
private static final long serialVersionUID = 7249069246763182397L;
//初始容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
//初始加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//初始的并发等级
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//最小的segment数量
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
//最大的segment数量
static final int MAX_SEGMENTS = 1 << 16; // slightly conservative
//在锁定之前,大小和containsValue方法中的不同步重试次数。
//如果表进行了连续修改,将无法获得准确的结果,
//则使用此方法可以避免无界重试。
static final int RETRIES_BEFORE_LOCK = 2;
concurrentHashMap的put操作 分为两步:
1、定位到segment
2、判断是否需要对segment中的HashEntry数组进行扩容,然后再在segment中进行插入操作
@SuppressWarnings("unchecked")
public V put(K key, V value) {
//
Segment<K,V> s;
//concurrentHashMap中key和value都不能为null,会抛出空指针异常
if (value == null)
throw new NullPointerException();
//对key进行hash,
int hash = hash(key);
//先是无符号右移 segmentShift 位 再 & segmentMask 得到j的值
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
//这一步是找到插入的位置 下面将其介绍此方法
s = ensureSegment(j);
// 将键值对保存到对应的segment中
return s.put(key, hash, value, false);
}
ensureSegment方法
ensureSegment用于确定指定的Segment是否存在,不存在则会创建
使用getObjectVolatile()方法提供的原子读语义获取指定Segment,如果为空,以构造ConcurrentHashMap对象时创建的Segment为模板,创建新的Segment。ensureSegment在创建Segment期间为不断使用getObjectVolatile()检查指定Segment是否为空,防止其他线程已经创建了Segment。
private Segment<K,V> ensureSegment(int k) {
final Segment<K,V>[] ss = this.segments;
long u = (k << SSHIFT) + SBASE; // raw offset
Segment<K,V> seg;
//判断Segment的getObjectVolatile()是否为空,为空进行创建新的segment
//创建Segment期间为不断使用getObjectVolatile()检查指定Segment是否为空,
//防止其他线程已经创建了Segment
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
Segment<K,V> proto = ss[0]; // use segment 0 as prototype
int cap = proto.table.length;
float lf = proto.loadFactor;
int threshold = (int)(cap * lf);
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // recheck
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
HashEntry:
HashEntry将value和next声明为volatile ,是为了保证内存可见性,也就是每次读取都将从内存读取最新的值,而不会从缓存读取。同时,写入next域也使用volatile写入语义保证原子性。写入使用原子性操作,读取使用volatile,从而保证了多线程访问的线程安全性。
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;
}
final void setNext(HashEntry<K,V> n) {
UNSAFE.putOrderedObject(this, nextOffset, n);
}
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);
}
}
}
put的第一步走完,已经找到位置。
Segment中的put方法:
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//对其进行尝试性加锁 失败就进行 scanAndLockForPut操作,
//这个方法后会返回hashEntry节点,(要么有一个要么是重新创建一个出来)
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);//下面介绍这个方法
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
//先获取需要put的<k,v>对在当前这个segment中对应的链表的表头结点。
HashEntry<K,V> first = entryAt(tab, index);
//遍历first的头节点的链表
for (HashEntry<K,V> e = first;;) {
//e不为空,说明有hash冲突
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;//新值覆盖旧值
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;//未进入上面的循环,e对应的key不是想要的,进行下一个
}
else {//e为空
if (node != null)//判断是否等于null 不等于就直接进行放值
node.setNext(first);
else//有可能这是第一次尝试性获取锁成功,所以给他一个new个节点
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
//判断它是不是大于它的阈值,大于进行扩容操作
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
//new出来一个新的hashEntry数组,对他们进行重哈希,这是二倍扩容
//扩容完成后将这个节点插入进去
rehash(node);
else
//无需扩容将其直接插入指定的index中进去
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
scanAndLockForPut
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // negative while locating node
//尝试性加锁失败、
//那么就对segment[hash]对应的链表进行遍历找到需要
//put的这个entry所在的链表中的位置
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
//尝试性获取锁失败初始retries = -1 进入下面的步骤
if (retries < 0) {
//判断e是不是null,是通过hash找到的这个节点,刚好是空,或者就是最后一个节点
if (e == null) {
if (node == null) //大胆的创建节点 如果node是null就new一个节点
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;//将retries 改为0
}
else if (key.equals(e.key))//遍历过程中发现我们要找的地方
retries = 0;//将其改为0
else
e = e.next;//不是我们要找的,否则找下一个节点
}
//前置++判断是否超过我们之前设置最大次数,
//好像是64次,超过就将其锁住,进入阻塞等待
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f; //有可能是别的线程将其更改,则重新遍历
retries = -1;
}
}
return node;
}
(图片均为网上下载 侵删)