当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还没有初始化

  1. private final Node<K,V>[] initTable() {
  2. Node<K,V>[] tab; int sc;
  3. while ((tab = table) == null || tab.length == 0) {
  4. if ((sc = sizeCtl) < 0) //sizeCtl默认值为0
  5. Thread.yield();
  6. //当所得值小于0时 yield方法让会当前线程让出执行权限
  7. //重新回到就绪状态 等待cpu调度
  8. else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
  9. //当有多个线程同时进入了 U.compareAndSwapInt只能保证有一个线程的结果为true
  10. //然后设置sizeCtl的值为-1 那么就保证了其他线程无法进入当前else if分支
  11. //其他线程也不会阻塞 而是继续走while循环 然后一直走 if ((sc = sizeCtl) < 0) {Thread.yield();}
  12. //因为sizeCtl值为-1<0 所以会一直走if循环
  13. try {
  14. //此时就只有一个线程进入到在各个分支中
  15. //双重检查来初始化数组
  16. if ((tab = table) == null || tab.length == 0) {
  17. int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
  18. @SuppressWarnings("unchecked")
  19. Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
  20. table = tab = nt;
  21. sc = n - (n >>> 2);
  22. }
  23. } finally {
  24. sizeCtl = sc;
  25. }
  26. break;
  27. //当数组初始化完成后 此时while循环中的 (tab = table) == null 判断条件就不成立了 所以整个数组初始化完毕 返回数组
  28. }
  29. }
  30. return tab;
  31. }