首先我们先对并发容器做一个简单的介绍:
ConcurrentHashMap代替同步的Map(Collections.synchronized(new HashMap())),众所周知,HashMap是根据散列值分段存储的,同步Map在同步的时候锁住了所有的段,而ConcurrentHashMap加锁的时候根据散列值锁住了散列值锁对应的那段,因此提高了并发性能。ConcurrentHashMap也增加了对常用复合操作的支持,比如”若没有则添加”:putIfAbsent(),替换:replace()。这2个操作都是原子操作。
CopyOnWriteArrayList和CopyOnWriteArraySet分别代替List和Set,主要是在遍历操作为主的情况下来代替同步的List和同步的Set,这也就是上面所述的思路:迭代过程要保证不出错,除了加锁,另外一种方法就是”克隆”容器对象。
ConcurrentLinkedQuerue是一个先进先出的队列。它是非阻塞队列。
ConcurrentSkipListMap可以在高效并发中替代SoredMap(例如用Collections.synchronzedMap包装的TreeMap)。
ConcurrentSkipListSet可以在高效并发中替代SoredSet(例如用Collections.synchronzedSet包装的TreeMap)。
我们看看一看ConcurrentHashMap的数据结构:
从这个图,我们可以清晰的看到ConcurrentHashMap的数据结构,首先它有一个个的Segment(),然后在通过Segment访问到一个个的hashEntry,可以看到其实一个Segment的结构跟hashmap的结构是一样的,不了解的同学可以去http://blog.csdn.net/cyq12345_/article/details/78866063。
我们都知道hashmap不是线程安全的,我们可以他变成线程安全的,但是在多线程操作的时候,我们需要锁住整个表,那么效率会非常的低,但是我们可以看到ConcurrentHashMap的数据结构就可以很好的解决这个问题,我们并发编程的时候可以锁住对应的Segment就可以了。
我们来看看segment的数据结构:
static final class Segment<K,V> extends ReentrantLock implements Serializable {
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;
}
讲讲这些参数的意思:
count:segment的数量。
modCount:修改的次数,一个非常重要的参数。
threshold:阈值,用来扩容。
loadFactor:加载因子,用来确定阈值。
再来看看hashEntry的数据结构
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;
}
我们接下来看看构造函数
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;
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;
}
这是最复杂的一个构造函数。我们来分析一下。
首先是对参数的合法性进行验证。
initialCapacity:初始化容量,表示的事是一个Segment的容量。
loadFactor:加载因子。
concurrencyLevel:这个代表初始化的时候Segment的数量,这个值很关键,因为这个值只要确定以后就不能再做跟改了。为了以后rehash的时候,就只重新计算segment里面的hashEntry的位置就行了,不必要重新计算segment的位置。
segmentShift ,segmentMask 这来两个元素也很重要,用来定位segment的位置的参数。
我们来看看put()方法
我直接在代码里面写注解了
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
//首先通过key的hashcode找到hash值
int hash = hash(key);
//通过运算取到应该存在哪个segment里面
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
//通过一个CAS算法,返回的一个segment
s = ensureSegment(j);
//然后在调用segment里面的put方法
return s.put(key, hash, value, false);
}
Segment中的put方法
还是直接在代码里面写注解
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;
//通过运算得到,存储在segment的hashEntry的位置
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;
//修改次数加1
++modCount;
}
break;
}
e = e.next;
}
else {
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;
}
get操作:
直接在代码里面写注解
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
//通过key的hascode算出一个hash
int h = hash(key);
//通过算出的hash做运算,算出存在的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);
e != null; e = e.next) {
K k;
//循环判断key是否存在,存在返回value的值
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
看到整个get操作,很简单,没有用到加锁的过程。
remove操作
public V remove(Object key) {
//根据key的hashcode算出一个hash
int hash = hash(key);
//然后通过hash进行运算得到一个Segment
Segment<K,V> s = segmentForHash(hash);
//判断segment,不为空进行remove操作。
return s == null ? null : s.remove(key, hash, null);
}
//这个是算segment位置的函数
private Segment<K,V> segmentForHash(int h) {
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
return (Segment<K,V>) UNSAFE.getObjectVolatile(segments, u);
}
//进行删除的函数
final V remove(Object key, int hash, Object value) {
//可以看到,这个也是加锁了
if (!tryLock())
scanAndLock(key, hash);
V oldValue = null;
try {
HashEntry<K,V>[] tab = table;
//算出存在哪个hashEntry里面
int index = (tab.length - 1) & hash;
HashEntry<K,V> e = entryAt(tab, index);
HashEntry<K,V> pred = null;
while (e != null) {
K k;
HashEntry<K,V> next = e.next;
//循环比较,key存在就删除
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
V v = e.value;
if (value == null || value == v || value.equals(v)) {
if (pred == null)
setEntryAt(tab, index, next);
else
pred.setNext(next);
//记录操作的次数
++modCount;
--count;
oldValue = v;
}
break;
}
pred = e;
e = next;
}
} finally {
//释放锁
unlock();
}
return oldValue;
}
rehash
private void rehash(HashEntry<K,V> node) {
//先把旧数组存起来
HashEntry<K,V>[] oldTable = table;
//获得旧数据的长度
int oldCapacity = oldTable.length;
//计算得到新数组的长度
int newCapacity = oldCapacity << 1;
//重新计算阈值
threshold = (int)(newCapacity * loadFactor);
//创建一个先的hashEntry,大小为刚算出来的。
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
int sizeMask = newCapacity - 1;
//循环旧数组
for (int i = 0; i < oldCapacity ; i++) {
HashEntry<K,V> e = oldTable[i];
if (e != null) {
//获得下一个节点
HashEntry<K,V> next = e.next;
//从新计算存储的下标
int idx = e.hash & sizeMask;
//判断是否为单节点
if (next == null) // Single node on list
//如果是单节点,直接赋值
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;
}
在前面的章节中,我们涉及到的操作都是在单个Segment中进行的,但是ConcurrentHashMap有一些操作是在多个Segment中进行,比如size操作,ConcurrentHashMap的size操作也采用了一种比较巧的方式,来尽量避免对所有的Segment都加锁。
前面我们提到了一个Segment中的有一个modCount变量,代表的是对Segment中元素的数量造成影响的操作的次数,这个值只增不减,size操作就是遍历了两次Segment,每次记录Segment的modCount值,然后将两次的modCount进行比较,如果相同,则表示期间没有发生过写入操作,就将原先遍历的结果返回,如果不相同,则把这个过程再重复做一次,如果再不相同,则就需要将所有的Segment都锁住,然后一个一个遍历了,具体的实现大家可以看ConcurrentHashMap的源码,这里就不贴了。