1.HashTable
1.来源
自JDK1.0开始就有的类,继承自Direction类,并实现了Map接口,这一块可自行看源码
2.字段说明
private transient Entry<?,?>[] table; 一个存放数据的hashtable
private transient int count; 放在hashTable中的节点的总数
private int threshold; 需要rehash时的临界值(capacity * loadFactor)
private float loadFactor; 加载因子(默认0.75,在调用无加载因子构造方法时默认给定)
private transient int modCount = 0; hashTable结构被修改的次数统计
3.主要方法
1)核心构造方法
public Hashtable(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
table = new Entry<?,?>[initialCapacity];
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
根据给定容量capacity和加载因子loadFactor,创建散列表,计算扩容临界值
2)另外两种形式的构造方法
public Hashtable() {
this(11, 0.75f);
}
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
第一个无参构造方法,设定默认容量11,加载因子0.75,调用了核心的构造方法来创建散列表
第二个构造方法,根据给定容量capacity,和默认给定的加载因子0.75,调用了核心的构造方法来创建散列表
3)添加元素的put方法
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
//判断该key已经存在,直接修改value的值,并结束方法返回
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
//插入元素
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
//判断count >= 临界值,进行rehash重新散列,并重新计算key的hash值和散列表中index位置
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
我们可以看到,1.这个方法是用synchronized关键字来修饰的,说明这个方法是对这个对象进行了加锁访问,也就是说是线程安全的。2.这个方法可以看到,如果value是null,是会抛出空指针异常的。3.根据计算出的hash值进行散列来插入元素,for循环用来判断若该key已经存在。4.若key不存在则调用addEntry方法进行元素的插入(插入元素如果有冲突,hashTable采用的是数组加链表的头插法),下面我们看下Entry的构造方法
4)Entry构造方法
protected Entry(int hash, K key, V value, Entry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
结合3中的方法,插入时取出了index位置的元素,并将他作为新插入节点的next节点,这样做的好处是省去了插入时查找末端元素的时间消耗
5)rehash
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// overflow-conscious code
//扩容后,新capacity为原来的2倍+1
int newCapacity = (oldCapacity << 1) + 1;
//判断capacity是否达到边界
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
//重新计算扩容临界值
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
//将旧元素放入新的散列表,冲突仍是采用头插进行
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
重新散列,扩容后为什么要2倍再+1呢,这是为了最大可能的使被散列的元素分布均匀
6)remove方法
public synchronized V remove(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
modCount++;
if (prev != null) {
prev.next = e.next;
} else {
tab[index] = e.next;
}
count--;
V oldValue = e.value;
e.value = null;
return oldValue;
}
}
return null;
}
这个方法也是用了synchronized来修饰,删除具体细节就是链表的元素删除
7)get方法
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
这个方法也用了synchronized来修饰,这说明这个类足够安全。至于元素的查找主要就是数组下标为index的链表查找
8)contains方法
public synchronized boolean contains(Object value) {
if (value == null) {
throw new NullPointerException();
}
Entry<?,?> tab[] = table;
for (int i = tab.length ; i-- > 0 ;) {
for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) {
if (e.value.equals(value)) {
return true;
}
}
}
return false;
}
这个方法,用了两个for循环来查找,第一个for循环用来遍历数组的下标,第二for循环用来查找数组下标下的链表中的元素
2.HashMap
1.来源
jdk1.2 继承自AbstractMap类,并实现了Map接口,AbstractMap类也实现了Map接口
2.字段说明
static final int DEFAULT_INITIAL_CAPACITY =1 <<4; HashMap的默认容量,16
static final int MAXIMUM_CAPACITY =1 <<30; HashMap的最大支持容量,2^30
static final float DEFAULT_LOAD_FACTOR =0.75f; HashMap的默认加载因子
static final int TREEIFY_THRESHOLD =8; Bucket中链表长度大于该默认值,转化为红黑树
static final int UNTREEIFY_THRESHOLD =6; Bucket中红黑树存储的Node小于该默认值,转化为链表
static final int MIN_TREEIFY_CAPACITY =64; 桶中的Node被树化时最小的hash表容量。(当桶中Node的数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行resize扩容操作这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。)
transient Node[] table; 存储元素的数组,总是2的n次幂
transient Set>entrySet; 存储具体元素的集
transient int size; HashMap中存储的键值对的数量
transient int modCount; HashMap扩容和结构改变的次数。
int threshold; 扩容的临界值,=容量*填充因子
final float loadFactor; 填充因子
3.主要方法
1)指定容量和负载因子的构造方法
public HashMap(int initialCapacity,float loadFactor) {
if (initialCapacity <0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
if (initialCapacity >MAXIMUM_CAPACITY)
initialCapacity =MAXIMUM_CAPACITY;
if (loadFactor <=0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
this.loadFactor = loadFactor;
this.threshold =tableSizeFor(initialCapacity);
}
这个构造方法来确定负载因子和扩容临界值,最后调用tableSizeFor方法来计算扩容临界值
2)其他两种构造方法
//无参构造方法
public HashMap() {
this.loadFactor =DEFAULT_LOAD_FACTOR;// all other fields defaulted
}
//指定容量的构造方法
public HashMap(int initialCapacity) {
this(initialCapacity,DEFAULT_LOAD_FACTOR);
}
无参构造方法,设定了负载因子为0.75的默认值。指定容量的构造方法设定容量,将负载因子设置为默认值
这两种构造方法,最后会调用3中的指定容量和负载因子的构造方法
3)根据键值对数量获取HashMap容量方法 tableSizeFor
static final int tableSizeFor(int cap) {
int n = cap -1;
n |= n >>>1;
n |= n >>>2;
n |= n >>>4;
n |= n >>>8;
n |= n >>>16;
return (n <0) ?1 : (n >=MAXIMUM_CAPACITY) ?MAXIMUM_CAPACITY : n +1;
}
tabSizeFor方法,主要根据传入的键值对容量,来返回大于容量的最小的二次幂数值。
算法如下:
将传入的容量-1:至于这里为什么需要减1,是为了防止cap已经是2的幂。如果cap已经是2的幂, 又没有执行这个减1操作,则执行完后面的几条无符号右移操作之后,返回的capacity将是这个cap的2倍。
假设原始n: 0001 xxxx xxxx xxxx
第一次右移1位+或运算:二进制序列出现至少两个连续的1,如 0001 1xxx xxxx xxxx;
第二次右移2位+或运算:二进制序列出现至少四个连续的1,如 0001 111x xxxx xxxx;
第三次右移4位+或运算:二进制序列出现至少八个连续的1, 如 0001 1111 1111 xxxx;
第四次右移8位+或运算:二进制序列至少出现16个连续的1,如 0001 1111 1111 1111;
第五次右移16位+或运算:二进制序列至少出现32个连续的1,如 0001 1111 1111 1111;
上述运算中,若出现右移后为0,则或运算得到的结果和原始值一致,则后续推导过程可以忽略。
此时可以保证,原始序列从包含1的最高位,到最低位,全部都变成了1.
最后+1,返回的结果就是大于原值的最小二次幂数。
4)hash方法
static final int hash(Object key) {
int h;
return (key ==null) ?0 : (h = key.hashCode()) ^ (h >>>16);
}
hash方法用传入的key的hashCode和hashCode无符号右移16位的结果,做异或运算后作为hash值返回。
注:之所以获取hashCode后,还需要和右移16位的hashCode做异或运算,原因是:在根据hash值获取键值对在bucket数组中的下标时,采用的算法是:index=h & (length-1),当数组的length较小时,只有低位能够参与到“与”运算中,但是将hashCode右移16位再与本身做异或获取到的hash,可以使高低位均能够参与到后面的与运算中。
下面图说明:
5)插入方法 putVal
final V putVal(int hash,K key,V value,boolean onlyIfAbsent,boolean evict) {
//存储Node节点的数组tab,单个Node节点p,HashMap的容量n
Node<K,V>[] tab; Node<K,V> p;int n, i;
//初始化数组桶table (首次进入初始化)
if ((tab =table) ==null || (n = tab.length) ==0)
n = (tab = resize()).length;
//如果数组桶中不包含要插入的元素,将新键值对作为新Node存入数组(当前tab中无冲突元素)
if ((p = tab[i = (n -1) & hash]) ==null)
tab[i] = newNode(hash, key, value,null);
else {//桶中包含要插入的元素(tab中已有元素,发生冲突)
//e用来保存当前key已存在时的节点
Node<K,V> e;K k;
//如果key和链表第一个元素p的key相等
if (p.hash == hash &&((k = p.key) == key || (key !=null && key.equals(k))))
e = p;
//若p是TreeNode类型,则使用红黑树的方法插入到树中
else if (pinstanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {//键值对的引用不在链表的第一个节点,此时需要遍历链表
for (int binCount =0; ; ++binCount) {
//将p.next指向e,并判断p是否为最后一个节点,若是插入新节点,此时e==null
if ((e = p.next) ==null) {
p.next = newNode(hash, key, value,null);
// -1 for 1st ,若链表长度大于等于8,变红黑树,退出遍历
if (binCount >=TREEIFY_THRESHOLD -1)
treeifyBin(tab, hash);
break;
}
//找到与当前key值相同的节点
if (e.hash == hash &&((k = e.key) == key
|| (key !=null &&key.equals(k))))
break;
//退出遍历(此时,e指向与当前key值相同的旧节点)
//将e指向p,便于下次遍历e = p.next
p = e;
}
}
//当e非空时,说明e是原来HashMap中的元素,具有和新节点一样的key值
if (e !=null) {
V oldValue = e.value;
//onlyIfAbsent 表示是否仅在 oldValue 为 null 的情况下更新键值对的值
if (!onlyIfAbsent || oldValue ==null)
e.value = value;
//空实现,LinkedHashMap用
afterNodeAccess(e);
return oldValue;
}
}
++modCount; //HashMap结构更改,modCount+1
if (++size >threshold) //判断是否需要扩容
resize();
afterNodeInsertion(evict); //空实现,LinkedHashMap用
return null;
}
HashMap中进行存储的入口方法是:put(K,V),但是核心方法是putVal方法,该方法一共有以下步骤:
1.初始化数组桶
2.判断数组桶中对应下标是否无元素存在,是,就直接存入
3.若数组桶中对应下标有元素存在,则开始遍历,根据长度将元素存入链表尾部或树中。
4.判断是否需要扩容
6)扩容方法 resize
final Node[] resize() {
Node[] oldTab =table;
int oldCap = (oldTab ==null) ?0 : oldTab.length; //原HashMap的容量
int oldThr =threshold; //原HashMap的扩容临界值
int newCap, newThr =0;
if (oldCap >0) { //case1 : odlCap>0,说明桶数组已经初始化过
if (oldCap >=MAXIMUM_CAPACITY) { //原HashMap的越界检查
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap <<1) < MAXIMUM_CAPACITY && oldCap >=DEFAULT_INITIAL_CAPACITY) //容量扩大一倍后的越界检查
newThr = oldThr <<1;// double threshold
}
//case2:oldCap=0 && oldThr >0,桶数组尚未初始化,当调用带初始化容量的构造函数时会发生该情况
else if (oldThr >0)// initial capacity was placed in threshold
newCap = oldThr; //在前面HashMap的初始化中,将Initial capcity暂存在threshold中,新的阀值按照下面newThr==0中的公式进行计算
//case3:oldCap=0 && oldThr = 0,当调用无参构造函数时会发生该情况,此时使用默认容量初始化
else {// zero initial threshold signifies using defaults
newCap =DEFAULT_INITIAL_CAPACITY; //默认容量
newThr = (int)(DEFAULT_LOAD_FACTOR *DEFAULT_INITIAL_CAPACITY); //默认扩容临界值
}
if (newThr ==0) { //case2中,调用Initial capcity构造方法时,该阀值为0,需计算阀值
float ft = (float)newCap *loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? 35(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap]; //上面获取到的新的Capcity,来创建一个新的桶数组 newTab,并指向table
table = newTab;
if (oldTab !=null) { //若oldTab非空,则需要将原来桶数组的元素取出来放到新的桶数组中
for (int j =0; j < oldCap; ++j) {
Node e;
if ((e = oldTab[j]) !=null) {
oldTab[j] =null; //将原桶数组的元素占用的空间释放,便于GC
if (e.next ==null) //若桶中元素的next为空,获取index后直接将其放入新桶数组中
newTab[e.hash & (newCap -1)] = e;
else if (einstanceof TreeNode) //若桶中元素的next是树节点
((TreeNode)e).split(this, newTab, j, oldCap); //采用树的方式插入
else {// preserve order 若桶中元素的next是链表节点
Node loHead =null, loTail =null;
Node hiHead =null, hiTail =null;
Node next;
do {
next = e.next;
if ((e.hash & oldCap) ==0) { //若e.e.hash & oldCap 结果为0,则下标在新桶数组中不用改变,此时,将元素存放在loHead为首的链表中
if (loTail ==null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else { //若e.e.hash & oldCap 结果不为0,则下标在新桶数组等于原下标+oldCap,此时,将元素存放在hiHead为首的链表中
if (hiTail ==null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
}while ((e = next) !=null);
if (loTail !=null) { //将小于原来容量的头节点放入原来数组中位置
loTail.next =null;
newTab[j] = loHead;
}
if (hiTail !=null) { //将大于原来容量的头节点放入(原来数组+扩容大小)中位置
hiTail.next =null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
/*原始链表中的元素,在resize之后,其下标有两种可能,一种是在原来下标处,另一种是原来下标+oldCap处
*举例说明: 若原来的容量 -1后 只有n位,低位有n个1,去下标公式为:i = (oldCap - 1) & hash,若hash值只有低n为有值,则与运算后获得的值和
*扩容前是一样的,若hash不止第n位有值,那采用与运算后,结果比原来刚好大oldCap。 下面有图片示例)
*/
上述代码分析较长,总结如下:
1.获取不同情况下的 新的容量 和 新的扩容临界值
2.根据新容量创建新的桶数组tab。
3.根据节点类型,树节点和链表节点分别采用对应方法放入新的桶数组
7)查找元素方法 getNode
final Node getNode(int hash, Object key) {
Node[] tab; Node first, e;int n;K k;
if ((tab =table) !=null && (n = tab.length) >0 && (first = tab[(n -1) & hash]) !=null) { //根据hash值,获取对应下标的第一个元素first
if (first.hash == hash && ((k = first.key) == key || (key !=null && key.equals(k))))// always check first node 如果first的key和待查询的key相等,返回first
return first;
if ((e = first.next) !=null) { //若first不是待查询的元素
if (firstinstanceof TreeNode) //若first是树节点,采用树节点的方式获取
return ((TreeNode)first).getTreeNode(hash, key);
do {
if (e.hash == hash && ((k = e.key) == key || (key !=null && key.equals(k)))) //first是链表节点头,使用循环获取
return e;
}while ((e = e.next) !=null);
}
}
return null;
}
查询元素的入口方法是:public V get(Object key),返回值是node的value,核心方法是getNode(int hash, Object key)。
8)删除元素 removeNode方法
final Node removeNode(int hash, Object key, Object value, boolean matchValue,boolean movable) {
Node[] tab; Node p;int n, index;
//通过hash值获取下标,下标对应的节点p不为空
if ((tab =table) !=null && (n = tab.length) >0 && (p = tab[index = (n -1) & hash]) !=null) {
Node node =null, e;K k;V v;
if (p.hash == hash && ((k = p.key) == key || (key !=null && key.equals(k)))) //若节点p的key和待移除的节点key相等
node = p; //将p指向待移除节点
else if ((e = p.next) !=null) { //p的key和待移除的节点key不相等,遍历p作为头的链表或者树
if (pinstanceof TreeNode) //采用树节点方式获得要移除的节点
node = ((TreeNode)p).getTreeNode(hash, key);
else { //p是链表的头节点
do {
//采用循环,当p.next不为空,比对它和传入的key,直到找到相等的key
if (e.hash == hash && ((k = e.key) == key || (key !=null && key.equals(k)))) {
node = e; //找到后,将节点指向node
break; //将e指向待移除节点,此时相当于p.next就是待移除的节点node
}
p = e;
}while ((e = e.next) !=null);
}
}
//若node非空,传入的matchValue参数为flase或 node的value等于传入value
if (node !=null && (!matchValue || (v = node.value) == value || (value !=null && value.equals(v)))) {
if (nodeinstanceof TreeNode) //若node是树节点
((TreeNode)node).removeTreeNode(this, tab, movable);
else if (node == p) //若待移除节点是链表头,将其指向待移除元素的next,移除对node的引用
tab[index] = node.next;
else //待移除元素是链表中的元素,此时其等于p.next
p.next = node.next; //将p.next指向node.next,移除了对node的引用
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
移除节点的入口方法是: public V remove(Object key) ,其核心方法是removeNode,主要做了以下几个工作:
1.通过用key获取的hash,来获取下标。
2.若下标对应处无元素,返回null。
3.若下标对应处有元素,判断是树或者链表,采用对应方法移除。
3.ConcurrentHashMap
1.来源
自jdk1.5,继承了AbstractMap类并实现了ConcurrentMap接口
2.字段说明
private static final int MAXIMUM_CAPACITY = 1 << 30; 最大容量
private static final int DEFAULT_CAPACITY = 16; 默认容量
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; 最大数组的size
private static final int DEFAULT_CONCURRENCY_LEVEL = 16; 默认的同步级别
private static final float LOAD_FACTOR = 0.75f; 默认加载因子
private static final int MIN_TRANSFER_STRIDE = 16;
private static int RESIZE_STAMP_BITS = 16;
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
3.主要方法
1)核心构造方法
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
//保证初始容量不小于同步级别
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
//计算容量
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
1.该构造方法包含了三个参数,初始容量initialCapacity,加载因子loadFactor,同步级别concurrencyLevel。
2.初始容量不能小于同步级别,若小于将初始容量设置为同步界别的值
3.计算容量时的tableSizeFor方法与HashMap中一致
4.sizeCtl == cap容量
2)其他构造方法
/**
* Creates a new, empty map with the default initial table size (16).
*/
//无参构造方法
public ConcurrentHashMap() {
}
//指定容量构造方法
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
//指定容量和加载因子构造方法
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}
3)put方法
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
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();
//判断table的i节点是否为null
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;
//对数组对应节点及该节点下的所有节点进行同步访问
synchronized (f) {
if (tabAt(tab, i) == f) {
fh>0 说明这个节点是一个链表的节点 不是树的节点
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
//如果hash值和key值相同 则修改对应结点的value值
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) {
//如果链表长度已经达到临界值8 就需要把链表转换为树结构
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//将当前ConcurrentHashMap的元素数量+1
addCount(1L, binCount);
return null;
}
put方法最后调用的都是putVal方法,主要有一下几点需要注意
1.不允许key和value为null
2.当该key计算出数组中index的下标为空时,使用了CAS(compareAndSwap)来放入该Node节点。放入成功直接返回,否则进入下个else if判断。
3.对key对应数组下标的第一个节点进行同步访问(synchronized)
4)get方法
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
//计算hash值
int h = spread(key.hashCode());
//根据hash值确定节点位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
//如果搜索到的节点key与传入的key相同且不为null,直接返回这个节点
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
//如果eh<0 说明这个节点在树上 直接寻找
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
//否则遍历链表 找到对应的值并返回
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
get方法比较简单,给定一个key来确定value的时候,必须满足两个条件 key相同 hash值相同,对于节点可能在链表或树上的情况,需要分别去查找.
5)replaceNode方法
//remove方法
public V remove(Object key) {
return replaceNode(key, null, null);
}
//真正删除节点的方法
final V replaceNode(Object key, V value, Object cv) {
//计算hash值
int hash = spread(key.hashCode());
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//null,直接break
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null)
break;
//如果当前tab在扩容中,参加到扩容操作中
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
//否则去删除元素
else {
V oldVal = null;
boolean validated = false;
//对f节点加锁,进行同步访问
synchronized (f) {
fh>0 说明这个节点是一个链表的节点 不是树的节点
if (tabAt(tab, i) == f) {
if (fh >= 0) {
validated = true;
for (Node<K,V> e = f, pred = null;;) {
K ek;
//找到节点删除节点
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
V ev = e.val;
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) {
oldVal = ev;
if (value != null)
e.val = value;
else if (pred != null)
pred.next = e.next;
else
setTabAt(tab, i, e.next);
}
break;
}
pred = e;
if ((e = e.next) == null)
break;
}
}
//如果是树节点则进行树节点的删除
else if (f instanceof TreeBin) {
validated = true;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
if ((r = t.root) != null &&
(p = r.findTreeNode(hash, key, null)) != null) {
V pv = p.val;
if (cv == null || cv == pv ||
(pv != null && cv.equals(pv))) {
oldVal = pv;
if (value != null)
p.val = value;
else if (t.removeTreeNode(p))
setTabAt(tab, i, untreeify(t.first));
}
}
}
}
}
//成功删除,count-1,返回oldVal
if (validated) {
if (oldVal != null) {
if (value == null)
addCount(-1L, -1);
return oldVal;
}
break;
}
}
}
return null;
}
我们可以看到remove方法最后会调用到replaceNode方法,这个方法在执行删除操作时,对数组的下标元素进行了加锁保证,对该节点的链表或树进行同步访问,保证了线程安全
6)keySet方法
public KeySetView<K,V> keySet() {
KeySetView<K,V> ks;
//传入ConcurrentHashMap自己本身
return (ks = keySet) != null ? ks : (keySet = new KeySetView<K,V>(this, null));
}
public static class KeySetView<K,V> extends CollectionView<K,V,K>
implements Set<K>, java.io.Serializable {
private static final long serialVersionUID = 7249069246763182397L;
private final V value;
//构造方法,获取传入的ConcurrentHashMap
KeySetView(ConcurrentHashMap<K,V> map, V value) { // non-public
super(map);
this.value = value;
}
/**
* Returns the default mapped value for additions,
* or {@code null} if additions are not supported.
*
* @return the default mapped value for additions, or {@code null}
* if not supported
*/
//返回传入ConcurrentHashMap的value
public V getMappedValue() { return value; }
/**
* {@inheritDoc}
* @throws NullPointerException if the specified key is null
*/
//调用传入ConcurrentHashMap的containsKey方法
public boolean contains(Object o) { return map.containsKey(o); }
/**
* Removes the key from this map view, by removing the key (and its
* corresponding value) from the backing map. This method does
* nothing if the key is not in the map.
*
* @param o the key to be removed from the backing map
* @return {@code true} if the backing map contained the specified key
* @throws NullPointerException if the specified key is null
*/
//调用传入ConcurrentHashMap的remove方法
public boolean remove(Object o) { return map.remove(o) != null; }
/**
* @return an iterator over the keys of the backing map
*/
//对传入的map,创建一个Key的迭代器
public Iterator<K> iterator() {
Node<K,V>[] t;
ConcurrentHashMap<K,V> m = map;
int f = (t = m.table) == null ? 0 : t.length;
return new KeyIterator<K,V>(t, f, 0, f, m);
}
/**
* Adds the specified key to this set view by mapping the key to
* the default mapped value in the backing map, if defined.
*
* @param e key to be added
* @return {@code true} if this set changed as a result of the call
* @throws NullPointerException if the specified key is null
* @throws UnsupportedOperationException if no default mapped value
* for additions was provided
*/
//add方法也是调用了传入ConcurrentHashMap的putVal方法
public boolean add(K e) {
V v;
if ((v = value) == null)
throw new UnsupportedOperationException();
return map.putVal(e, v, true) == null;
}
/**
* Adds all of the elements in the specified collection to this set,
* as if by calling {@link #add} on each one.
*
* @param c the elements to be inserted into this set
* @return {@code true} if this set changed as a result of the call
* @throws NullPointerException if the collection or any of its
* elements are {@code null}
* @throws UnsupportedOperationException if no default mapped value
* for additions was provided
*/
public boolean addAll(Collection<? extends K> c) {
boolean added = false;
V v;
if ((v = value) == null)
throw new UnsupportedOperationException();
for (K e : c) {
if (map.putVal(e, v, true) == null)
added = true;
}
return added;
}
public int hashCode() {
int h = 0;
for (K e : this)
h += e.hashCode();
return h;
}
public boolean equals(Object o) {
Set<?> c;
return ((o instanceof Set) &&
((c = (Set<?>)o) == this ||
(containsAll(c) && c.containsAll(this))));
}
public Spliterator<K> spliterator() {
Node<K,V>[] t;
ConcurrentHashMap<K,V> m = map;
long n = m.sumCount();
int f = (t = m.table) == null ? 0 : t.length;
return new KeySpliterator<K,V>(t, f, 0, f, n < 0L ? 0L : n);
}
public void forEach(Consumer<? super K> action) {
if (action == null) throw new NullPointerException();
Node<K,V>[] t;
if ((t = map.table) != null) {
Traverser<K,V> it = new Traverser<K,V>(t, t.length, 0, t.length);
for (Node<K,V> p; (p = it.advance()) != null; )
action.accept(p.key);
}
}
}
keySet方法中其核心就是调用了其静态内部类KeySetView<K,V>,其大部分方法都是调用了传入的ConcurrentHashMap本身的field或者method。另外一些method进行了自己的处理。
4.三个类的对比
1.线程安全性
从源码看,HashTable和ConcurrentHashMap是线程安全的,而HashMap是非线程安全的。HashTable与ConcurrentHashMap最本质的区别就是性能问题。从源码我们分析出,HastTable是对整个table进行加锁,这样在多线程访问时,只允许单线程操作;而ConcurrentHashMap则是依据每个table中的节点进行加锁,这样在不发生冲突的理想情况下,是可以同时保证table.length()多个线程进行操作。所以推荐用ConcurrentHashMap。至于HashMap,没有考虑多线程操作这种情况,至于不安全的地方网上一搜一大把。
2.key value值问题
Hashtable中key和value都不能为null,HashMap中, key可以为null,但这样的健值只有一个,ConcurrentHashMap中,key和value都不允许空值
3.遍历方式
ConcurrentHashMap、HashMap使用了Iterator, Hashtable使用了Enumeration的方式
4.扩容方式
HashTable中hash数组默认大小为11, 增加的方式是old*2+1, ConcurrentHashMap、HashMap数组的默认大小是16, 而且一定是2的指数,扩容也是原来的2倍
码字不易,有不对的地方欢迎指正。这只是初步的分析,对于ConcurrentHashMap这个类的精髓还需要多读才能get到。