Map
特点:key 唯一,无序
基本API
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class MyMap {
public static void main(String[] args) {
/**
* Map<K, V>:----key----value "键值对"
* API
*
* 增加:put() putAll()
* 删除:clear() remove()
* 修改:replace()
* 查询:size() get() entrySet() keySet() values()
* 判断:equals() isEmpty() containsValue() containsKey()
*/
Map<Integer, String> map = new HashMap<Integer, String>();
map.put(0, "hello");// 存入的键值对都是由映射关系的
map.put(15, "java");
map.put(18, "python");
map.put(5, "java");
map.put(0, "php");
System.out.println(map.keySet()); //=> [0, 18, 5, 15],返回此映射中包含的键的Set视图。
System.out.println(map.entrySet()); //=> [0=php, 18=python, 5=java, 15=java],返回此映射中包含的映射的Set视图。
System.out.println(map.values()); //=> [php, python, java, java],返回此映射中包含的值的Collection视图。
System.out.println(map.containsKey(0)); //=> true
System.out.println(map.containsValue("java")); //=> true
System.out.println(map.replace(0, "Json")); //=> "php",执行key进行更新操作,返回替换前的数据
System.out.println(map.replace(0, "js", "Javascript")); //=> false,对执行的映射进行更新操作成功返回true,否则返回false
System.out.println(map); //=> {0=php, 18=python, 5=java, 15=java},特点:key唯一,无序
System.out.println(map.size()); //=> 4
System.out.println(map.get(0));
// 遍历
Set<Integer> keySet = map.keySet();
for (Integer o :
keySet) {
System.out.println(map.get(o));
}
System.out.println("------------------");
Collection<String> keySet2 = map.values();
for (String o :
keySet2) {
System.out.println(o);
}
// 内部接口作为泛型
// entrySet() 方法返回的是Set 类型,有泛型,泛型为:Entry 类型--->是Map 接口中的内部接口
// 这个接口的泛型又有两个类型Integer ,String
System.out.println("------------------");
Set<Map.Entry<Integer, String>> entries = map.entrySet();
for (Map.Entry<Integer, String> o :
entries) {
System.out.println(o.getKey() + "-----" + o.getValue());
}
}
}
HashMap 实现类
特点:key 唯一,无序
基本API
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class MyMap {
public static void main(String[] args) {
/**
* Map<K, V>:----key----value "键值对"
* API
*
* 增加:put() putAll()
* 删除:clear() remove()
* 修改:replace()
* 查询:size() get() entrySet() keySet() values()
* 判断:equals() isEmpty() containsValue() containsKey()
*/
Map<Integer, String> map = new HashMap<Integer, String>();
map.put(0, "hello");// 存入的键值对都是由映射关系的
map.put(15, "java");
map.put(18, "python");
map.put(5, "java");
map.put(0, "php");
System.out.println(map.keySet()); //=> [0, 18, 5, 15],返回此映射中包含的键的Set视图。
System.out.println(map.entrySet()); //=> [0=php, 18=python, 5=java, 15=java],返回此映射中包含的映射的Set视图。
System.out.println(map.values()); //=> [php, python, java, java],返回此映射中包含的值的Collection视图。
System.out.println(map.containsKey(0)); //=> true
System.out.println(map.containsValue("java")); //=> true
System.out.println(map.replace(0, "Json")); //=> "php",执行key进行更新操作,返回替换前的数据
System.out.println(map.replace(0, "js", "Javascript")); //=> false,对执行的映射进行更新操作成功返回true,否则返回false
System.out.println(map); //=> {0=php, 18=python, 5=java, 15=java},特点:key唯一,无序
System.out.println(map.size()); //=> 4
System.out.println(map.get(0));
// 遍历
Set<Integer> keySet = map.keySet();
for (Integer o :
keySet) {
System.out.println(map.get(o));
}
System.out.println("------------------");
Collection<String> keySet2 = map.values();
for (String o :
keySet2) {
System.out.println(o);
}
// 内部接口作为泛型
// entrySet() 方法返回的是Set 类型,有泛型,泛型为:Entry 类型--->是Map 接口中的内部接口
// 这个接口的泛型又有两个类型Integer ,String
System.out.println("------------------");
Set<Map.Entry<Integer, String>> entries = map.entrySet();
for (Map.Entry<Integer, String> o :
entries) {
System.out.println(o.getKey() + "-----" + o.getValue());
}
}
}
加入要使用引用类型作为key 要怎么做?
必须重写hashCode()和equals()方法
import java.util.*;
class Test{
public int age;
public String name;
public Test(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Test)) return false;
Test test = (Test) o;
return age == test.age && name.equals(test.name);
}
@Override
public int hashCode() {
return Objects.hash(age, name);
}
@Override
public String toString() {
return "Test{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
public class MyMap {
public static void main(String[] args) {
/**
* 使用引用类型作为 key
* 必须重写 hashCode()和 equals()
*/
Map<Test, String> map2 = new HashMap<Test, String>();
map2.put(new Test(12,"xiaoming"),"1");
map2.put(new Test(12,"xiaoming"),"1");
map2.put(new Test(13,"xiaoming"),"1");
System.out.println("--------------------");
System.out.println(map2); //=> {Test{age=13, name='xiaoming'}=1, Test{age=12, name='xiaoming'}=1}
System.out.println(map2.size()); //=> 2
}
}
源码分析:
public class MyMap {
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<Integer, String>();
map.put(0, "hello");
System.out.println(map);
}
}
存储方式是主数组 + 链表
底层构造器与put 方法
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 装填因子
// 这就是底层储存用的主数组
// 这个数组的类型是 Node<K,V> 类对象
transient Node<K,V>[] table;
final float loadFactor;
transient int size;
int threshold; // 接收承重
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
// 获取hashCode
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* 内部类对象
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;//------- 哈希码(int)
final K key;//------- 键值(Integer)
V value;//------- 数据(String)
Node<K,V> next;//------- 指向下一个结点(Node<Integer,String>)
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
// 重写toString方法
public final String toString() { return key + "=" + value; }
// 重写hashCode方法保证唯一性
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
// 重写equals方法保证唯一性
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length; //----0
int oldThr = threshold; //----0
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 进行数组扩容 double threshold double newCap
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY; //----16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //----12
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr; //----12
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //----长度 16 的数组
table = newTab; // 长度为16的主数组
if (oldTab != null) {
// 扩容后的数组对原数组进行转移
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
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;
}
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
// 初始化主数组
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
// 在主数组某一位置上第一次添加元素
tab[i] = newNode(hash, key, value, null);
else {
// 在主数组某一位置上多次添加元素
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
// 超出承重范围就重新扩容 threshold:12----24----48 ...
resize();
afterNodeInsertion(evict);
return null;
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
}
底层get 方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 执行循环遍历数组通过key 进行查询
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
Hashtable 实现类
基本API 效果与HashMap 相同。
区别:
-
JDK版本
(1)HashMap:JDK1.2 效率高线程不安全
(2)Hashtable:JDK1.0 效率低线程较安全
-
数据储存问题
(1)Hashtable 不能储存null 数据(key && value)。
(2)HashMap key 和value 都可以是null ,且key 的null 也是唯一的。
LinkedHashMap 实现类
特点:按照添加顺序排列,唯一
基本API 效果与HashMap 相同。
import java.util.*;
class Test{
public int age;
public String name;
public Test(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Test)) return false;
Test test = (Test) o;
return age == test.age && name.equals(test.name);
}
@Override
public int hashCode() {
return Objects.hash(age, name);
}
@Override
public String toString() {
return "Test{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
public class MyMap {
public static void main(String[] args) {
Map<Test, String> map2 = new LinkedHashMap<Test, String>();
map2.put(new Test(12,"xiaoming"),"1");
map2.put(new Test(12,"xiaoming"),"1");
map2.put(new Test(13,"xiaoming"),"1");
map2.put(new Test(80,"xiaoming"),"1");
map2.put(new Test(55,"xiaoming"),"1");
System.out.println("--------------------");
System.out.println(map2); //=> {Test{age=12, name='xiaoming'}=1, Test{age=13, name='xiaoming'}=1, Test{age=80, name='xiaoming'}=1, Test{age=55, name='xiaoming'}=1}
System.out.println(map2.size()); //=> 4
}
}
TreeMap 实现类
特点:key有序,唯一
基本API 效果与HashMap 相同。
注意如果key 是引用类型则该引用类型的类必须实现Comparable 接口,并且重写compareTo()方法或者使用内部类直接定义比较器.
- 实现Comparable 接口,并且重写compareTo()方法
import java.util.*;
class Test implements Comparable {
public int age;
public String name;
@Override
public int compareTo(Object o) {
return this.age - ((Test) o).age;
}
public Test(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Test)) return false;
Test test = (Test) o;
return age == test.age && name.equals(test.name);
}
@Override
public int hashCode() {
return Objects.hash(age, name);
}
@Override
public String toString() {
return "Test{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
public class MyMap {
public static void main(String[] args) {
Map<Test, String> map2 = new TreeMap<Test, String>();
map2.put(new Test(12,"xiaoming"),"1");
map2.put(new Test(12,"xiaoming"),"1");
map2.put(new Test(13,"xiaoming"),"1");
map2.put(new Test(80,"xiaoming"),"1");
map2.put(new Test(55,"xiaoming"),"1");
System.out.println("--------------------");
System.out.println(map2); //=> {Test{age=12, name='xiaoming'}=1, Test{age=13, name='xiaoming'}=1, Test{age=55, name='xiaoming'}=1, Test{age=80, name='xiaoming'}=1}
System.out.println(map2.size()); //=> 4
}
}
- 定义内部比较器
import java.util.*;
class Test {
public int age;
public String name;
public Test(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Test)) return false;
Test test = (Test) o;
return age == test.age && name.equals(test.name);
}
@Override
public int hashCode() {
return Objects.hash(age, name);
}
@Override
public String toString() {
return "Test{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
public class MyMap {
public static void main(String[] args) {
Map<Test, String> map2 = new TreeMap<Test, String>(new Comparator<Test>() {
@Override
public int compare(Test o1, Test o2) {
return o1.age - o2.age;
}
});
map2.put(new Test(12, "xiaoming"), "1");
map2.put(new Test(12, "xiaoming"), "1");
map2.put(new Test(13, "xiaoming"), "1");
map2.put(new Test(80, "xiaoming"), "1");
map2.put(new Test(55, "xiaoming"), "1");
System.out.println("--------------------");
System.out.println(map2); //=> {Test{age=12, name='xiaoming'}=1, Test{age=13, name='xiaoming'}=1, Test{age=55, name='xiaoming'}=1, Test{age=80, name='xiaoming'}=1}
System.out.println(map2.size()); //=> 4
}
}
ConcurrentHashMap实现类
在 Java 中,HashMap 是非线程安全的,如果想在多线程下安全的操作 map,有哪些解决方法呢?
-
第一种方法,使用Hashtable线程安全类;
-
第二种方法,使用Collections.synchronizedMap方法,对方法进行加同步锁;
-
第三种方法,使用并发包中的ConcurrentHashMap类;
Hashtable 是一个线程安全的类,Hashtable 几乎所有的添加、删除、查询方法都加了synchronized同步锁!
相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞等待需要的锁被释放,在竞争激烈的多线程场景中性能就会非常差,所以 Hashtable 不推荐使用!
Collections.synchronizedMap 里面使用对象锁来保证多线程场景下,操作安全,本质也是对 HashMap 进行全表锁!
使用Collections.synchronizedMap方法,在竞争激烈的多线程环境下性能依然也非常差,所以不推荐使用!
ConcurrentHashMap 类所采用的是分段锁的思想,将 HashMap 进行切割,把 HashMap 中的哈希数组切分成小数组,每个小数组有 n 个 HashEntry 组成,其中小数组继承自ReentrantLock(可重入锁),这个小数组名叫Segment, 如下图:
当然,JDK1.7 和 JDK1.8 对 ConcurrentHashMap 的实现有很大的不同!
JDK1.8 对 HashMap 做了改造,当冲突链表长度大于8时,会将链表转变成红黑树结构,上图是 ConcurrentHashMap 的整体结构,参考 JDK1.7!
我们再来看看 JDK1.8 中 ConcurrentHashMap 的整体结构,内容如下:
JDK1.8 中 ConcurrentHashMap 类取消了 Segment 分段锁,采用 CAS + synchronized 来保证并发安全,数据结构跟 jdk1.8 中 HashMap 结构类似,都是数组 + 链表(当链表长度大于8时,链表结构转为红黑二叉树)结构。
ConcurrentHashMap 中 synchronized 只锁定当前链表或红黑二叉树的首节点,只要节点 hash 不冲突,就不会产生并发,相比 JDK1.7 的 ConcurrentHashMap 效率又提升了 N 倍!
说了这么多,我们再一起来看看 ConcurrentHashMap 的源码实现。
JDK1.7 中的 ConcurrentHashMap Segment是什么呢?
Segment本身就相当于一个HashMap对象。
同HashMap一样,Segment包含一个 HashEntry 数组,数组中的每一个 HashEntry 既是一个键值对,也是一个链表的头节点。
单一的Segment结构如下:
像这样的Segment对象,在ConcurrentHashMap集合中有多少个呢?有2的N次方个,共同保存在一个名为segments的数组当中。 因此整个ConcurrentHashMap的结构如下:
可以说,ConcurrentHashMap 是一个二级哈希表。在一个总的哈希表下面,有若干个子哈希表。
这样的二级结构,和数据库的水平拆分有些相似。
下面我们看看ConcurrentHashMap并发读写的几种情况?
Case1:不同Segment的并发写入(可以并发执行)
Case2:同一Segment的一写一读(可以并发执行)
Case3:同一Segment的并发写入
Segment的写入是需要上锁的,因此对同一Segment的并发写入会被阻塞。
由此可见,ConcurrentHashMap 中每个Segment各自持有一把锁。在保证线程安全的同时降低了锁的粒度,让并发操作效率更高。
接下来,我们就来看下ConcurrentHashMap读写的过程。
- get方法
为输入的Key做Hash运算,得到hash值。
通过hash值,定位到对应的Segment对象
再次通过hash值,定位到Segment当中数组的具体位置。
- put方法
为输入的Key做Hash运算,得到hash值。
通过hash值,定位到对应的Segment对象
获取可重入锁
再次通过hash值,定位到Segment当中数组的具体位置。
插入或覆盖HashEntry对象。
释放锁。
JDK1.8 中的 ConcurrentHashMap
虽然 JDK1.7 中的 ConcurrentHashMap 解决了 HashMap 并发的安全性,但是当冲突的链表过长时,在查询遍历的时候依然很慢!
在 JDK1.8 中,HashMap 引入了红黑二叉树设计,当冲突的链表长度大于8时,会将链表转化成红黑二叉树结构,红黑二叉树又被称为平衡二叉树,在查询效率方面,又大大的提高了不少。
JDK1.8 中的ConcurrentHashMap 相比 JDK1.7 中的 ConcurrentHashMap, 它抛弃了原有的 Segment 分段锁实现,采用了 CAS + synchronized 来保证并发的安全性。
JDK1.8 中的 ConcurrentHashMap 对节点Node类中的共享变量,和 JDK1.7 一样,使用volatile关键字,保证多线程操作时,变量的可见性!
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;
}
...
}
下面来具体分析一下JDK1.8 中的 ConcurrentHashMap源码。
put方法
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
// key和value都不能为null
if (key == null || value == null) throw new NullPointerException();
// 计算hash值
int hash = spread(key.hashCode());
int binCount = 0;
for (ConcurrentHashMap.Node<K, V>[] tab = table; ; ) {
ConcurrentHashMap.Node<K, V> f;
int n, i, fh;
if (tab == null || (n = tab.length) == 0)
// 如果tab未初始化或者个数为0,则初始化node数组
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new ConcurrentHashMap.Node<K, V>(hash, key, value, null)))
// 如果使用CAS插入元素时,发现已经有元素了,则进入下一次循环,重新操作;如果使用CAS插入元素成功,则break跳出循环,流程结束
break;
} else if ((fh = f.hash) == MOVED)
// 如果要插入的元素所在的tab的第一个元素的hash是MOVED,则当前线程帮忙一起迁移元素
tab = helpTransfer(tab, f);
else {
// 如果这个tab不为空且不在迁移元素,则锁住这个tab(分段锁)
// 并查找要插入的元素是否在这个tab中
// 存在,则替换值(onlyIfAbsent=false)
// 不存在,则插入到链表结尾或插入树中
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
// 如果第一个元素的hash值大于等于0(说明不是在迁移,也不是树)
// 那就是tab中的元素使用的是链表方式存储
if (fh >= 0) {
// tab中元素个数赋值为1
binCount = 1;
// 遍历整个tab,每次结束binCount加1
for (ConcurrentHashMap.Node<K, V> e = f; ; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
// 如果找到了这个元素,则赋值了新值(onlyIfAbsent=false)
// 并退出循环
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
ConcurrentHashMap.Node<K, V> pred = e;
if ((e = e.next) == null) {
// 如果到链表尾部还没有找到元素
// 就把它插入到链表结尾并退出循环
pred.next = new ConcurrentHashMap.Node<K, V>(hash, key,
value, null);
break;
}
}
} else if (f instanceof ConcurrentHashMap.TreeBin) { // 如果第一个元素是树节点
ConcurrentHashMap.Node<K, V> p;
// tab中元素个数赋值为2
binCount = 2;
// 调用红黑树的插入方法插入元素
// 如果成功插入则返回null
// 否则返回寻找到的节点
if ((p = ((ConcurrentHashMap.TreeBin<K, V>) f).putTreeVal(hash, key,
value)) != null) {
// 如果找到了这个元素,则赋值了新值(onlyIfAbsent=false)
// 并退出循环
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
// 如果binCount不为0,说明成功插入了元素或者寻找到了元素
if (binCount != 0) {
// 因为上面把元素插入到树中时,binCount只赋值了2,并没有计算整个树中元素的个数
// 所以不会重复树化
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
// 如果要插入的元素已经存在,则返回旧值
if (oldVal != null)
return oldVal;
// 退出外层大循环,流程结束
break;
}
}
}// 成功插入元素,元素个数加1(是否要扩容在这个里面)
addCount(1L, binCount);
// 成功插入元素返回null
return null;
}
当进行 put 操作时,流程大概可以分如下几个步骤:
-
首先会判断 key、value是否为空,如果为空就抛异常;
-
接着会判断容器数组是否为空,如果为空就初始化数组;
-
进一步判断,要插入的元素f,在当前数组下标是否第一次插入,如果是就通过 CAS 方式插入;
-
在接着判断f.hash == -1是否成立,如果成立,说明当前f是ForwardingNode节点,表示有其它线程正在扩容,则一起进行扩容操作;
-
其他的情况,就是把新的Node节点按链表或红黑树的方式插入到合适的位置;
-
节点插入完成之后,接着判断链表长度是否超过8,如果超过8个,就将链表转化为红黑树结构;
-
最后,插入完成之后,进行扩容判断。
initTable 初始化数组
private final ConcurrentHashMap.Node<K, V>[] initTable() {
ConcurrentHashMap.Node<K, V>[] tab;
int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0) // 如果sizeCtl<0说明正在初始化或者扩容,让出CPU
Thread.yield();
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
// 如果把sizeCtl原子更新为-1成功,则当前线程进入初始化
// 如果原子更新失败则说明有其它线程先一步进入初始化了,则进入下一次循环
// 如果下一次循环时还没初始化完毕,则sizeCtl<0进入上面if的逻辑让出CPU
// 如果下一次循环更新完毕了,则table.length!=0,退出循环
try {
// 再次检查table是否为空,防止ABA问题
if ((tab = table) == null || tab.length == 0) { // 如果sc为0则使用默认值16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;// 新建数组
@SuppressWarnings("unchecked")
ConcurrentHashMap.Node<K, V>[] nt = (ConcurrentHashMap.Node<K, V>[]) new ConcurrentHashMap.Node<?, ?>[n];
// 把tab数组赋值给table
table = tab = nt;
// 设置sc为数组长度的0.75倍
// n - (n >>> 2) = n - n/4 = 0.75n
// 可见这里装载因子和扩容门槛都是写死了的
// 这也正是没有threshold和loadFactor属性的原因
sc = n - (n >>> 2);
}
} finally {
// 把sc赋值给sizeCtl,这时存储的是扩容门槛
sizeCtl = sc;
}
break;
}
}
return tab;
}
使用CAS锁控制只有一个线程初始化tab数组;
sizeCtl在初始化后存储的是扩容门槛;
扩容门槛写死的是tab数组大小的0.75倍,tab数组大小即map的容量,也就是最多存储多少个元素。
helpTransfer 协助扩容
final ConcurrentHashMap.Node<K, V>[] helpTransfer(ConcurrentHashMap.Node<K, V>[] tab, ConcurrentHashMap.Node<K, V> f) {
ConcurrentHashMap.Node<K, V>[] nextTab;
int sc;
// 如果tab数组不为空,并且当前tab第一个元素为ForwardingNode类型,并且nextTab不为空
// 说明当前tab已经迁移完毕了,才去帮忙迁移其它tab的元素
// 扩容时会把旧tab的第一个元素置为ForwardingNode,并让其nextTab指向新tab数组
if (tab != null && (f instanceof ConcurrentHashMap.ForwardingNode) &&
(nextTab = ((ConcurrentHashMap.ForwardingNode<K, V>) f).nextTable) != null) {
int rs = resizeStamp(tab.length);
// sizeCtl<0,说明正在扩容
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
// 扩容线程数加1
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
// 当前线程帮忙迁移元素
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
操作步骤如下:
-
第1步,对 table、node 节点、node 节点的 nextTable,进行数据校验;
-
第2步,根据数组的length得到一个标识符号;
-
第3步,进一步校验 nextTab、tab、sizeCtl 值,如果 nextTab 没有被并发修改并且 tab 也没有被并发修改,同时 sizeCtl < 0,说明还在扩容;
-
第4步,对 sizeCtl 参数值进行分析判断,如果不满足任何一个判断,将sizeCtl + 1, 增加了一个线程帮助其扩容。
addCount 扩容判断
private final void addCount(long x, int check) {
ConcurrentHashMap.CounterCell[] as;
long b, s;
// 先尝试把数量加到baseCount上,如果失败再加到分段的CounterCell上
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
ConcurrentHashMap.CounterCell a;
long v;
int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
// 计算元素个数
s = sumCount();
}
//检查是否需要扩容,默认check=1,需要检查
if (check >= 0) {
ConcurrentHashMap.Node<K, V>[] tab, nt;
int n, sc;
// 如果元素个数达到了扩容门槛,则进行扩容
// 注意,正常情况下sizeCtl存储的是扩容门槛,即容量的0.75倍
while (s >= (long) (sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
// rs是扩容时的一个标识
int rs = resizeStamp(n);
if (sc < 0) {// sc<0说明正在扩容中
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
// 扩容已经完成了,退出循环
break;
// 扩容未完成,则当前线程加入迁移元素中
// 并把扩容线程数加1
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
} else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2)) // 进入迁移元素
transfer(tab, null); // 重新计算元素个数
s = sumCount();
}
}
}
操作步骤如下:
第1步,利用CAS将方法更新baseCount的值
第2步,检查是否需要扩容,默认check = 1,需要检查;
第3步,如果满足扩容条件,判断当前是否正在扩容,如果是正在扩容就一起扩容;
第4步,如果不在扩容,将sizeCtl更新为负数,并进行扩容处理。
以上就是整个put方法的流程,可以从中发现,里面大量的使用了CAS方法,CAS 表示比较与替换,里面有3个参数,分别是目标内存地址、旧值、新值,每次判断的时候,会将旧值与目标内存地址中的值进行比较,如果相等,就将新值更新到内存地址里,如果不相等,就继续循环,直到操作成功为止!
get方法
public V get(Object key) {
ConcurrentHashMap.Node<K,V>[] tab; ConcurrentHashMap.Node<K,V> e, p; int n, eh; K ek;
// 计算hash
int h = spread(key.hashCode());
// 判断数组是否为空,通过key定位到数组下标是否为空;
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 如果第一个元素就是要找的元素,直接返回
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
// hash小于0,说明是树或者正在扩容
// 使用find寻找元素,find的寻找方式依据Node的不同子类有不同的实现方式
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;
}
步骤如下:
-
第1步,判断数组是否为空,通过key定位到数组下标是否为空;
-
第2步,判断node节点第一个元素是不是要找到,如果是直接返回;
-
第3步,如果是红黑树结构,就从红黑树里面查询;
-
第4步,如果是链表结构,循环遍历判断。
reomve 方法
public V remove(Object key) {
return replaceNode(key, null, null);
}
final V replaceNode(Object key, V value, Object cv) {
int hash = spread(key.hashCode());
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null)
break;
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
boolean validated = false;
synchronized (f) {
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));
}
}
}
}
}
if (validated) {
if (oldVal != null) {
if (value == null)
addCount(-1L, -1);
return oldVal;
}
break;
}
}
}
return null;
}
步骤如下:
-
第1步,循环遍历数组,接着校验参数;
-
第2步,判断是否有别的线程正在扩容,如果是一起扩容;
-
第3步,用 synchronized 同步锁,保证并发时元素移除安全;
-
第4步,因为 check= -1,所以不会进行扩容操作,利用CAS操作修改baseCount值。
提问
ConcurrentHashMap不能解决什么问题呢?
请看下面的例子:
public static void main(String[] args) {
private static final Map<Integer, Integer> map = new ConcurrentHashMap<>();
public void unsafeUpdate (Integer key, Integer value){
Integer oldValue = map.get(key);
if (oldValue == null) {
map.put(key, value);
}
}
}
这里如果有多个线程同时调用unsafeUpdate()这个方法,ConcurrentHashMap还能保证线程安全吗?
答案是不能。因为get()之后if之前可能有其它线程已经put()了这个元素,这时候再put()就把那个线程put()的元素覆盖了。
那怎么修改呢?
答案也很简单,使用putIfAbsent()方法,它会保证元素不存在时才插入元素,如下:
public void safeUpdate(Integer key, Integer value) { map.putIfAbsent(key, value); } 复制代码
那么,如果上面oldValue不是跟 null 比较,而是跟一个特定的值比如1进行比较怎么办?
也就是下面这样:
public void unsafeUpdate (Integer key, Integer value){
Integer oldValue = map.get(key);
if (oldValue == 1) {
map.put(key, value);
}
}
这样的话就没办法使用putIfAbsent()方法了。
其实,ConcurrentHashMap还提供了另一个方法叫replace(K key, V oldValue, V newValue)可以解决这个问题。
replace(K key, V oldValue, V newValue)这个方法可不能乱用,如果传入的newValue是null,则会删除元素。
public void safeUpdate (Integer key, Integer value){
map.replace(key, 1, value);
}
那么,如果if之后不是简单的put()操作,而是还有其它业务操作,之后才是put(),比如下面这样,这该怎么办呢?
public void unsafeUpdate (Integer key, Integer value){
Integer oldValue = map.get(key);
if (oldValue == 1) {
System.out.println(System.currentTimeMillis());
/**
* 其它业务操作
*/
System.out.println(System.currentTimeMillis());
map.put(key, value);
}
}
这时候就没办法使用ConcurrentHashMap提供的方法了,只能业务自己来保证线程安全了,比如下面这样:
public void safeUpdate (Integer key, Integer value){
synchronized (map) {
Integer oldValue = map.get(key);
if (oldValue == null) {
System.out.println(System.currentTimeMillis());
/**
* 其它业务操作
*/
System.out.println(System.currentTimeMillis());
map.put(key, value);
}
}
}
这样虽然不太友好,但是最起码能保证业务逻辑是正确的。
当然,这里使用ConcurrentHashMap的意义也就不大了,可以换成普通的HashMap了。
我们不能听说ConcurrentHashMap是线程安全的,就认为它无论什么情况下都是线程安全的。
总结
虽然 HashMap 在多线程环境下操作不安全,但是在 java.util.concurrent 包下,java 为我们提供了 ConcurrentHashMap 类,保证在多线程下 HashMap 操作安全!
在 JDK1.7 中,ConcurrentHashMap 采用了分段锁策略,将一个 HashMap 切割成 Segment 数组,其中 Segment 可以看成一个 HashMap, 不同点是 Segment 继承自 ReentrantLock,在操作的时候给 Segment 赋予了一个对象锁,从而保证多线程环境下并发操作安全。
但是 JDK1.7 中,HashMap 容易因为冲突链表过长,造成查询效率低,所以在 JDK1.8 中,HashMap 引入了红黑树特性,当冲突链表长度大于8时,会将链表转化成红黑二叉树结构。
在 JDK1.8 中,与此对应的 ConcurrentHashMap 也是采用了与 HashMap 类似的存储结构,但是 JDK1.8 中 ConcurrentHashMap 并没有采用分段锁的策略,而是在元素的节点上采用 CAS + synchronized 操作来保证并发的安全性,源码的实现比 JDK1.7 要复杂的多。