源码为jdk1.8
Java7与Java8中的ConcurrentHashMap:
在ConcurrentHashMap中主要通过锁分段技术实现上述目标。
在Java7中,ConcurrentHashMap由Segment数组结构和HashEntry数组组成。
Segment(分段锁) ,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表,同时又是一个ReentrantLock(Segment继承了ReentrantLock)。 ConcurrentHashMap使用分段锁技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。
在Java8中,ConcurrentHashMap去除了Segment分段锁的数据结构,主要是基于CAS操作保证保证数据的获取以及使用synchronized关键字对相应数据段加锁实现了主要功能,这进一步提高了并发性。同时同时为了提高哈希碰撞下的寻址性能,Java 8在链表长度超过一定阈值时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(long(N)))。
ConcurrentHashMap介绍
ConcurrentHashMap 是concurrent包下的一个集合类。它是线程安全的哈希表。它是通过“分段锁”来实现多线程下的安全问题。它将哈希表分成了不同的段内使用了可重入锁(ReentrantLock ),不同线程只在一个段内存在线程的竞争。它不会对整个哈希表加锁。
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
它继承了AbstractMap类,实现了ConcurrentMap,Serializable接口
ConcurrentHashMap具有的属性
//默认容量大小
static final int DEFAULT_INITIAL_CAPACITY = 16;
//默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//默认并发数量(segments数组的大小)
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//最大并发数量
static final int MAX_SEGMENTS = 1 << 16;
//节点长度的阀值为8
static final int TREEIFY_THRESHOLD = 8;
目前ConcurrentHashMap采用Node类作为基本的存储单元,每个键值对(key-value)都存储在一个Node中。同时Node也有一些子类,TreeNodes用于树结构中(当链表长度大于一定阈值时将链表转化为红黑树);
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return val; }
public final int hashCode() { return key.hashCode() ^ val.hashCode(); }
public final String toString(){ return key + "=" + val; }
public final V setValue(V value) {
throw new UnsupportedOperationException();
}
public final boolean equals(Object o) {...};
/**
* Virtualized support for map.get(); overridden in subclasses.
*/
Node<K,V> find(int h, Object k) {...}
}
ConcurrentHashMap中的原子操作
在ConcurrentHashMap中通过原子操作查找元素、替换元素和设置元素。这些原子操作起着非常关键的作用。
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
ConcurrentHashMap的put方法
如果key或者value为null,则抛出空指针异常;
如果table为null或者table的长度为0,则初始化table,调用initTable()方法。
计算当前键值的索引位置,如果Hash表中当前节点为null,则将元素直接插入。(注意,这里使用的就是前面锁说的CAS操作)
如果当前位置的节点元素的hash值为-1,说明这是一个ForwaringNodes节点,即正在进行扩容。那么当前线程加入扩容。
当前节点不为null,对当前节点加锁,将元素插入到当前节点。在Java8中,当节点长度大于阀值时,就将节点转为树的结构。
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
//计算传入key的hash值,
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable(); //初始化哈希表
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//CAS操作插入
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//*重点:锁的是当前的Node节点 而不是整个hash表
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
//当key相同且它的hash值相同时,替换当前元素的值
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
//替换值
e.val = value;
break;
}
Node<K,V> pred = e;
//没有相同时候,插入到节点中
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
//如果节点变成树
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
ConcurrentHashMap与HashMap、HashTable的区别
1.ConcurrentHashMap和HashMap是2k扩容,HashTable扩容:2k+1;
2.ConcurrentHashMap 、HashTable线程安全;HashMap线程不安全;
3.ConcurrentHashMap 、HashTable不允许值为null,值为null时抛出异常,
HashMap允许值为null;
4.ConcurrentHashMap在jdk1.8中底层用的是synchronized+CAS+HashEntry+红黑树。
5.HashTable所有方法都加锁synchronized。当一个线程来访问时候,会锁定整个map表,其他线程则不 能去访问该map表;
ConcurrentHashMap采用了CAS+同步锁Synchronized对链表中Node<k,V>节点进行锁定;当一个线程访问时候,会锁住该Node节点,而其他线程过来若访问其他节点时候不影响,可以正常访问;