当HashMap在多线程环境下存在线程安全问题(添加、删除、扩容等会发生环状链 死循环)
所以一般通过使用Hashtable或ConcurrentHashMap来解决线程安全问题
不过处于线程并发度的原因 一般会使用ConcurrentHashMap 它的的性能和效率会比较高
Hahtable是通过synchronized实现同步的 get/put/remove都会加锁
所以Hashtable即使是拿数据 也会加锁 但是拿数据一般不会对数据产生什么影响
所以加锁就会导致性能很慢 就不能满足高并发的需求
ConcurrentHashMap的put方法
1)计算key的hash来确定位置 如果此时数组没有初始化 那么先通过initTable方法初始化
2)如果没有hash冲突 就创建一个Node节点 直接CAS无锁插入(key不能为null 会抛出异常)
这里是利用CAS来尝试写入数据的 CAS是乐观锁的一种实现方式(不加锁)
主要分为两个步骤:比较和修改
比较:线程读取到一个值A 在将其更新为值B之前 会比较原值是否仍为A
修改:如果是 将A修改为B 如果不是 就什么都不做
这就保证了只有一个线程能赋值成功 并不会发生线程阻塞的问题
CAS缺点:不能保证数据没被别的线程修改过 比如经典的ABA问题 CAS就无法判断了
ABA就是一个线程将值改为B 另一个线程又将值改回A
所以这个时候它就无法判断了
但是很多时候我们只需要追求最终的结果正确 所以在这里是没有关系的
3)若此时需要扩容 就先进行扩容
4)如果发生了hash冲突 就synchronized加锁来保证线程安全
有两种情况:
a)链表结构就直接遍历到尾部插入
b)红黑树结构就按照红黑树结构插入
5)count(记录链表节点的长度)判断count的值是否大于阈值8 大于就将链表 —> 红黑树
6)如果添加成功 需要判断数组中元素个数(size)是否超过当前数组大小阈值 超过就扩容
ConcurrentHashMap的initTable方法初始化数组
重要属性 private transient volatile int sizeCtl;
-1 表示正在初始化
-N 表示N-1个线程个线程正在进行扩容
0 表示table还没有初始化
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0) //sizeCtl默认值为0
Thread.yield();
//当所得值小于0时 yield方法让会当前线程让出执行权限
//重新回到就绪状态 等待cpu调度
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
//当有多个线程同时进入了 U.compareAndSwapInt只能保证有一个线程的结果为true
//然后设置sizeCtl的值为-1 那么就保证了其他线程无法进入当前else if分支
//其他线程也不会阻塞 而是继续走while循环 然后一直走 if ((sc = sizeCtl) < 0) {Thread.yield();}
//因为sizeCtl值为-1<0 所以会一直走if循环
try {
//此时就只有一个线程进入到在各个分支中
//双重检查来初始化数组
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
//当数组初始化完成后 此时while循环中的 (tab = table) == null 判断条件就不成立了 所以整个数组初始化完毕 返回数组
}
}
return tab;
}