前序
在容器Map中,HashMap对数据(key-value,简称mapping)的操作效率快,但是不加锁,不安全;虽然Hashtable对数据的操作是加锁的,安全,但是对象级加锁的,整个对象都加锁了,比如,现在需要对Hashtable进行put操作,而同时已经对Hashtable有get操作,则put操作需要等get操作完后释放对象的锁才继续进行,这样效率上会有所降低。即需要解决效率,也需要考虑问题,java提供了java.util.concurrent.ConcurrentHashMap类对此类问题有很大的帮助。
基本数据结构
ConcurrentHashMap的锁不是针对ConcurrentHashMap整个对象级的,它的锁是分段的,如果操作的数据不存在同个段,则不会有被锁的困扰。下面是ConcurrentHashMap类简要图:
ConcurrentHashMap对mapping是保存在数组Segment[],也可以说是分段保存,ConcurrentHashMap根据mapping的key的hash值生成段的下标index,则保存在segments[index]下,则key的hash值相等的mapping都保存在同个segments[index]下,这点跟HashMap是类似的。segments[index]即Segment对象,ConcurrentHashMap的锁是加在Segment对象上的,其它segments[index]是不受影响的,也可以说ConcurrentHashMap的锁是分段锁,这样,即使某一段被加锁了,其它段的操作是不受影响的,这样就加快了ConcurrentHashMap的效率。
生成maping所在段(segments[index])的下标的方法:
/**
* Applies a supplemental hash function to a given hashCode, which
* defends against poor quality hash functions. This is critical
* because ConcurrentHashMap uses power-of-two length hash tables,
* that otherwise encounter collisions for hashCodes that do not
* differ in lower or upper bits.
*/
private static int hash(int h) {
// Spread bits to regularize both segment and index locations,
// using variant of single-word Wang/Jenkins hash.
h += (h << 15) ^ 0xffffcd7d;
h ^= (h >>> 10);
h += (h << 3);
h ^= (h >>> 6);
h += (h << 2) + (h << 14);
return h ^ (h >>> 16);
}
Segment中使用数组HashEntry[]存储mapping,Segment根据mapping的key的hash值生成保存的数组下标index,如果Segment生成的下标是相同,则用HashEntry的next属性链接,由于可看ConcurrentHashMap也使用了列表、链表来保存数据。
Segment保存mapping在数据HashEntry[ ] 中的下标index,是这样生成的:
HashEntry<K,V>[] tab = table;
int index = hash & (tab.length - 1);
其中hash是key的哈希值,table是Segment中的数组HashEntry[] table。
以下是Segment和HashEntry:
由上面两个结构,可以看出Entry<K,V>的值V是volatile修饰的,Segment<K,V>的HashEntry<K,V>数组table也是volatile修饰的,这使得在一个线程下修改一个元素的值,或者数组table有变动(比如增加,删除元素),则其他所有线程都可以知道这个变动,从而达到数据变动的透明性。
ConcurrentHashMap中存储数据各个结构的关系如下:
下面从源码入手来看下元素是怎么插入ConcurrentHashMap的
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
implements ConcurrentMap<K, V>, Serializable {
final Segment<K,V>[] segments;
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long SBASE;
private static final int SSHIFT;
private static final long TBASE;
private static final int TSHIFT;
private static final long HASHSEED_OFFSET;
private static final long SEGSHIFT_OFFSET;
private static final long SEGMASK_OFFSET;
private static final long SEGMENTS_OFFSET;
static {
int ss, ts;
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class tc = HashEntry[].class;
Class sc = Segment[].class;
//UNSAFE.arrayBaseOffset(Class arrayClass):获取给定数组中第一个元素的偏移地址
TBASE = UNSAFE.arrayBaseOffset(tc);
SBASE = UNSAFE.arrayBaseOffset(sc);
//UNSAFE.arrayIndexScale(Class arrayClass):获取比例因子,这个比例因子可以用来对指定的数组类的元素进行寻址
ts = UNSAFE.arrayIndexScale(tc);
ss = UNSAFE.arrayIndexScale(sc);
HASHSEED_OFFSET = UNSAFE.objectFieldOffset(ConcurrentHashMap.class.getDeclaredField("hashSeed"));
SEGSHIFT_OFFSET = UNSAFE.objectFieldOffset(ConcurrentHashMap.class.getDeclaredField("segmentShift"));
SEGMASK_OFFSET = UNSAFE.objectFieldOffset(ConcurrentHashMap.class.getDeclaredField("segmentMask"));
//UNSAFE.objectFieldOffset(Field f):返回指定静态属性在内存地址中偏移量,这个偏移量仅仅是用作访问这个类的指定属性的一种方式。
SEGMENTS_OFFSET = UNSAFE.objectFieldOffset(ConcurrentHashMap.class.getDeclaredField("segments"));
} catch (Exception e) {
throw new Error(e);
}
if ((ss & (ss-1)) != 0 || (ts & (ts-1)) != 0)
throw new Error("data type scale not a power of two");
//Integer.numberOfLeadingZeros(int i)的解释说明:如下
//1024以2为底的对数:10
//10 = 31 - Integer.numberOfLeadingZeros(1024)
SSHIFT = 31 - Integer.numberOfLeadingZeros(ss);
TSHIFT = 31 - Integer.numberOfLeadingZeros(ts);
}
//插入元素
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
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的put方法插入元素
return s.put(key, hash, value, false);
}
//获取元素在ConcurrentHashMap中对应段(即Segment<K,V>[]数组中的Segment<K,V>元素)
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;
//UNSAFE.getObjectVolatile(Object obj, long offset):返回对象obj中指定地址偏移量的属性,支持volatile load语义
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) {
//以CAS的原子操作方式将数组Segment<K,V>[] ss中偏移量为u的位置更新为Segment<K,V> s
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
//..其他省略
}
下面看看Segment是如何插入元素的,其中我们来看看put方法。
static final class Segment<K,V> extends ReentrantLock{
//..其他省略
final V put(K key, int hash, V value, boolean onlyIfAbsent){
//获得锁
tryLock(); //tryLock() ==>> ReentrantLock.tryLock() ==>>sync.nonfairTryAcquire(1)
try{
//put 操作。。省略。。
}finally{
//释放锁sync.release(1),传值1,因为lock和unlock是一一对应的,获得锁一次,就要释放一次。
unlock(); //unlock ==>> sync.release(1);
}
}
}
ReentrantLock的相关代码
public class ReentrantLock implements Lock{
Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer{
//获得锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//state值为0,表示没有线程获得当前锁
if (c == 0) {
//进行CAS(比较并交换)设置,CAS是CPU的指令,硬件的CPU会保证CAS指令是原子指令操作
if (compareAndSetState(0, acquires)) {
//compareAndSetState方法返回true,表示设置成功,
//当前线程可以获得锁,并设置锁的持有人是当前线程:setExclusiveOwnerThread(current)
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {//表示锁的持有人是当前线程,即线程再次获得了锁,这是没问题的
int nextc = c + acquires;//因为acquires传进来的值是1,表示当前线程又一次获得该锁,nextc表示线程重复获得锁次数
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
//释放锁
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {//c为0,表示线程对锁已经全部释放,则锁是自由了free。(如果线程多次获得一个锁,则需要多次释放锁)
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
}
class NonfairSync extends Sync{..}
class FairSync extends Sync{..}
ReetrantLock(){
sync = new NonfairSync();
}
//...其他省略
}
AbstractQueuedSynchronizer的相关代码
public abstract class AbstractQueuedSynchronizer{
/**
* The synchronization state.
*
*/
//volation可以使得一旦state被修改,其他所有线程都可以知道state的最新值(即处理器的缓存区的缓存被设置无效,state需要重新从内存获取)
//初始化值为0,则表示锁没有被获得。
private volatile int state;
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long stateOffset;
static {
try {
//获取state的偏移量,以便使用CAS原子操作
stateOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
} catch (Exception ex) { throw new Error(ex); }
}
//CAS操作,修改state的值
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
}
想悉CAS(比较并交换)原理,可以查看点击打开链接
ConcurrentHashMap的弱一致性
JDK1.6源码
我们主要讨论get方法的弱一致性,即是说,ConcurrentHashMap的底层数据结构已经加入一个map<k,v>元素后,在get方法里有可能不能马上取到对应元素的值。
底层数据结构主要是Segment中的HashEntry<K,V>[] table保存了map<k,v>元素,也就是table已经保存了元素,在调用get方法时,有可能不能立刻取到对应元素。
下面结合源码和JMM来看看。
主要看java.util.concurrent.ConcurrentHashMap.Segment<K, V>的put,get方法
Segment的结构主要有: HashEntry<K,V>[] table,count是volatile的;
HashEntry的结构主要看: value是volatile的,则会有线程可见性。
V put(K key, int hash, V value, boolean onlyIfAbsent) {
lock();
try {
int c = count;
if (c++ > threshold) // ensure capacity
rehash();
HashEntry<K,V>[] tab = table;
int index = hash & (tab.length - 1);
HashEntry<K,V> first = tab[index];
HashEntry<K,V> e = first;
while (e != null && (e.hash != hash || !key.equals(e.key)))
e = e.next;
V oldValue;
if (e != null) { //1处,key已存在的情况
oldValue = e.value;
if (!onlyIfAbsent)
e.value = value; //2处,只需赋值value的值即可
}
else {
oldValue = null;
++modCount;
tab[index] = new HashEntry<K,V>(key, hash, first, value); //3处,创建一个新的HashEntry,然后再赋值给tab[index]
count = c; // write-volatile //4处,在3处赋值完tabl[index]后,再修改count的值,count是volatile的,对其他线程是可见性的。
}
return oldValue;
} finally {
unlock();
}
}
V get(Object key, int hash) {
if (count != 0) { // read-volatile //5处,需要在count值不为0才会获取值value
HashEntry<K,V> e = getFirst(hash); //6处
while (e != null) {
if (e.hash == hash && key.equals(e.key)) {
V v = e.value; //8处
if (v != null)
return v;
return readValueUnderLock(e); // recheck //7处
}
e = e.next;
}
}
return null;
}
HashEntry<K,V> getFirst(int hash) {
HashEntry<K,V>[] tab = table;
return tab[hash & (tab.length - 1)];
}
其中put方法从开始有加锁,到方法结束释放锁;而get方法开始是没有加锁的。
put方法分两个情况:
1.加入的元素<k,v>原先是已经存在的
即是在"1处"的,如果key是存在的,则对应"2处",只要修改value的值,由于value是volatile的即是对其他线程可见的,只要修改后,则是可以get得到的,不用等待put方法的结束,不会有弱一致性的问题。
2.加入的元素<k,v>原先是不存在的
即对应到"3处",需要创建一个新的HashEntry,然后再赋值给tab[index],在"3处"操作完之后 ,再"4处"操作,修改count的值,count是volatile的,对其他线程是可见性的。但对于get方法来说,需要判断count值不为0才会获取值value,即是"5处",如果刚好是"3处"操作完,底层数据结构已经保存了加入的元素,但是"4处"还没修改count值之前,就执行了"5处"操作,则会产生弱一致性的问题,get不到加入元素的value。
看到“7处”,当e.value为null时,会调用readValueUnderLock方法获取value值。
/**
* Reads value field of an entry under lock. Called if value
* field ever appears to be null. This is possible only if a
* compiler happens to reorder a HashEntry initialization with
* its table assignment, which is legal under memory model
* but is not known to ever occur.
*/
V readValueUnderLock(HashEntry<K,V> e) {
lock();
try {
return e.value;
} finally {
unlock();
}
}
对于“8处”,e.value,e是HashEntry是table的元素,虽然table数组是volatile的,但table的元素HashEntry并不是volatile的,在旧的JMM里,通过非volatile变量访问volatile变量是可以被编译器重排的(不过在新的JMM会加强volatile语义,几乎不会有这种情况),当编译器重排了HashEntry的初始化时,value域的值可能为null,会调用readValueUnderLock,但这种情况是很少出现的,所以这个方法只是个备用方案,这种重排对于旧的JMM(JSR 133之前)来说是合法的。
readValueUnderLock这里会加锁来使用同步获取value的值,根据JMM的Synchronization Order,unlock前的操作结果肯定对lock后的操作可见,即是unlock happen before lock,所以通过加锁能获取到之前value的赋值。
继续看源码对HashEntry的解释:
/**
* ConcurrentHashMap list entry. Note that this is never exported
* out as a user-visible Map.Entry.
*
* Because the value field is volatile, not final, it is legal wrt
* the Java Memory Model for an unsynchronized reader to see null
* instead of initial value when read via a data race. Although a
* reordering leading to this is not likely to ever actually
* occur, the Segment.readValueUnderLock method is used as a
* backup in case a null (pre-initialized) value is ever seen in
* an unsynchronized access method.
*/
static final class HashEntry<K,V> {
final K key;
final int hash;
volatile V value;
final HashEntry<K,V> next;
HashEntry(K key, int hash, HashEntry<K,V> next, V value) {
this.key = key;
this.hash = hash;
this.next = next;
this.value = value;
}
}
value域是volatile的,而不是final,在旧的JMM里,由于编译器可能对程序的重排,会导致非同步模式下的程序在有线程竞争之下可能看到null(即是HashEntry初始化之前的值)而不是初始值。
Java的final和volatile详细说明,可参照:
http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#finalRight
JDK1.7源码
主要看java.util.concurrent.ConcurrentHashMap.Segment<K, V>的put,get方法
Segment的HashEntry<K,V>[] table、HashEntry的value是volatile,Segment的count不是volatile的。
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & 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))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value; //61处
++modCount;
}
break;
}
e = e.next;
}
else {
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first); //6处,创建新的HashEntry
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node); //7处,将新的HashEntry设置到HashEntry[] table中
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
static final <K,V> void setEntryAt(HashEntry<K,V>[] tab, int i, HashEntry<K,V> e) {
UNSAFE.putOrderedObject(tab, ((long)i << TSHIFT) + TBASE, e);
}
static final <K,V> HashEntry<K,V> entryAt(HashEntry<K,V>[] tab, int i) {
return (tab == null) ? null : (HashEntry<K,V>) UNSAFE.getObjectVolatile(tab, ((long)i << TSHIFT) + TBASE);
}
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null && (tab = s.table) != null) { //8处
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
如果加入的元素是已经存在的,则也是修改HashEntry的value值就可以了,value就volatile的,不存在弱一致性问题;
如果加入的元素是不存在的,也不会存在弱一致性问题了,因为1.7源码中已经不再用count来作为判断条件来get元素的value了,而是使用UNSAFE.getObjectVolatile()方法可以直接拿到新加入的元素,即是"8处",volatile是可见性的,所以不存在弱一致性问题。