HashTable是一个线程安全的类,它使用synchronnized来锁住整张hash表来实现线程安全,即每次锁住整张表让线程独占。ConcurrentHashMap容许多个修改操做并发进行,其关键在于使用了锁分离的技术。它使用了多个锁来控制对hash表的不一样部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不一样的部分,每一个段其实就是一个小的Hashtable,他们有本身的锁。只要多个修改操做发生在不一样的段上,他们就能够并发进行。html
有些方法须要跨段,好比size()和containsValue(),他们可能须要锁定整个表而不只仅是某个段,这须要按顺序锁定全部段,操做完毕后,又按顺序释放全部段的锁。这里的“按顺序”是很重要的,不然极有可能出现死锁,在ConcurrentHashMap内部,段数组是final的,而且其成员变量实际上也是final的,可是,仅仅是将数组声明为final的并不保证数组成员也是final的,这须要实现上的保证。这能够确保不会出现死锁,由于得到锁的顺序是固定的。数组
实现原理:安全
ConcurrentHashMap使用分段锁技术,将数据分红一段一段的存储,而后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其余段的数据也能被其余线程访问,可以实现真正的并发访问。以下图是ConcurrentHashMap的内部结构图:多线程
从图中能够看到,ConcurrentHashMap内部分为不少个Segment,每个Segment拥有一把锁,而后每一个Segment(继承ReentrantLock)并发
static final class Segment<K,V> extends ReentrantLock implements Serializableapp
Segment继承了ReentrantLock,代表每一个segment均可以当作一个锁。(ReentrantLock前文已经提到,不了解的话就把当作synchronized的替代者吧)这样对每一个segment中的数据须要同步操做的话都是使用每一个segment容器对象自身的锁来实现。只有对全局须要改变时锁定的是全部的segment。ssh
Segment下面包含不少个HashEntry列表数组。对于一个key,须要通过三次(为何要hash三次下文会详细讲解)hash操做,才能最终定位这个元素的位置,这三次hash分别为:函数
ConcurrentHashMap中主要实体类就是三个:ConcurrentHashMap(整个Hash表),Segment(桶),HashEntry(节点),对应上面的图能够看出之间的关系this
/** spa
* The segments, each of which is a specialized hash table
*/
final Segment<K,V>[] segments;
不变(Immutable)和易变(Volatile)ConcurrentHashMap彻底容许多个读操做并发进行,读操做并不须要加锁。若是使用传统的技术,如HashMap中的实现,若是容许能够在hash链的中间添加或删除元素,读操做不加锁将获得不一致的数据。ConcurrentHashMap实现技术是保证HashEntry几乎是不可变的。HashEntry表明每一个hash链中的一个节点,其结构以下所示:
static final class HashEntry<K,V> {
final K key;
final int hash;
volatile V value;
volatile HashEntry<K,V> next;
}
在JDK 1.6中,HashEntry中的next指针也定义为final,而且每次插入将新添加节点做为链的头节点(同HashMap实现),并且每次删除一个节点时,会将删除节点以前的全部节点 拷贝一份组成一个新的链,而将当前节点的上一个节点的next指向当前节点的下一个节点,从而在删除之后 有两条链存在,于是能够保证即便在同一条链中,有一个线程在删除,而另外一个线程在遍历,它们都能工做良好,由于遍历的线程能继续使用原有的链。于是这种实现是一种更加细粒度的happens-before关系,即若是遍历线程在删除线程结束后开始,则它能看到删除后的变化,若是它发生在删除线程正在执行中间,则它会使用原有的链,而不会等到删除线程结束后再执行,即看不到删除线程的影响。若是这不符合你的需求,仍是乖乖的用Hashtable或HashMap的synchronized版本,Collections.synchronizedMap()作的包装。
而HashMap中的Entry只有key是final的
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
不变 模式(immutable)是多线程安全里最简单的一种保障方式。由于你拿他没有办法,想改变它也没有机会。
不变模式主要经过final关键字来限定的。在JMM中final关键字还有特殊的语义。Final域使得确保初始化安全性(initialization safety)成为可能,初始化安全性让不可变形对象不须要同步就能自由地被访问和共享。
先看看ConcurrentHashMap的初始化作了哪些事情,构造函数的源码以下:
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,loadFactor,concurrencyLevel这三个。
初始化的一些动做: