重新认识ConcurrentHashMap

ConcurrentHashMap如何控制并发

ConcurrentHashMap的加锁是不会把整个HashMap锁住的,锁住的是一个Hash槽位,提高了并发性能

总结就是:CAS + 锁Hash槽位 + volatile

从ConcurrentHashMap的两个方法来分析

  • put()

    通过上面CAS和Synchronized的分析,我们可以知道put方法是通过CAS和Synchronized保证的线程安全、数据一致性的

  • get()

    get()是不需要加锁的,put方法需要进行加锁,也就是说一个线程在进行写操作的时候,get方法也可以正常进行

    因为get()方法没有被锁住,注意跟Hashtable做对比。

    那为什么get()可以不加锁?

    因为ConcurrentHashMap用来存放数据的Node数组是用volatile进行修饰的,所有修改操作都可以第一时间被感知到

    所以哪怕在写操作中进行get也是可以的,只要写操作在get操作结束前完成,get操作一样会拿到最新修改的值

    可是这也引出来一个问题,那要是写操作一直到get操作结束后还没完成该怎么办?这个就是我们要讨论的读写冲突的问题

既然你说put的时候要用synchronized进行加锁,而get得时候不用加锁,这不是产生冲突了吗,这不是违背了本意吗?

其实这个问题就是在问你,你对ConcurrentHashMap读写冲突问题的理解

我将读写冲突问题分为两类

  1. 读操作和写操作同时发生在一个槽位,写操作在读操作结束前完成

    这种读写冲突是可以完美解决的,因为ConcurrentHashMap存储数据的Node数组是由volatile修饰的,只要读操作没有完成

    写操作结束后,所有线程第一时间都会获得最新的值

  2. 读操作和写操作同时发生在一个槽位,写操作在读操作结束后完成

    这种读写冲突是无法避免的,我们称之为弱一致性,弱一致性指的是,线程进行读操作可能会得到旧的值,因为新的值还没修改成功

分析一下volatile

volatile修饰的变量,只能从主存中取数据,不在每个线程的工作内存中取(这一块涉及jvm的内存模型的一些知识)

所以volatile在一定程度上保证了可见性,因为所有操作都是操作的主存中的数据,而非工作内存

volatile不保证原子性,这个怎么理解?

java 复制代码
volatile int a = 0;
a++;

这里的a涉及两三个操作,先从主存中拿到a的值,然后把a的值+1,再写入主存。

在拿到a的值到修改a的值前的这段时间,a可能已经被修改了

那现在来看这个问题:若violate i=0, 有3个线程同时对其+1,i的值是多少?最终的结果是不确定的,可能是1、2或3

ConcurrentHashMap的并发扩容是什么

在ConcurrentHashMap的多线程扩容过程中,多个线程可以协作进行数据迁移",指的是多个线程并发地将旧数据重新插入到新的数组(也就是扩容后的新表)中。

过程具体如下:

  1. 初始化新表:扩容开始时,会创建一个新的、容量更大的Node数组(表)。
  2. 标记迁移 :正在迁移的桶会被设置为一个特殊标记的Node,即forwardingNode,它的hash值为MOVED == -1,这用来告诉其他线程这个桶正在迁移过程中。
  3. 多线程参与 :当某个线程发现桶被标记为迁移状态时,它可以选择调用helpTransfer方法来协助数据迁移,而不是立即返回或等待。这样,不只是发起扩容的线程,其他访问到这些正在迁移桶的线程也可以参与到迁移工作中,形成了多线程并发迁移的局面。
  4. 并发迁移数据:参与迁移的线程会遍历旧表的每个桶,将其中的节点重新哈希并插入到新表的相应位置。由于新表的容量更大,这个过程通常伴随着重新分配,即原来在一个桶中的节点可能分散到新表的多个桶中。
  5. 完成迁移并原子切换:一旦所有桶的数据迁移完成,会通过CAS操作原子性地将新表设置为当前表,旧表则被废弃,这个过程确保了所有线程都能看到最新、已扩容的表,同时避免了数据不一致的问题。

ConcurrentHashMap锁的是什么

这个需要我们通过源码的形式进行分析,源码较长,这里只放出核心部分

java 复制代码
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; K fk; V fv;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                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);
                                    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;
                            }
                        }
                        else if (f instanceof ReservationNode)
                            throw new IllegalStateException("Recursive update");
                    }
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

synchronized (f) 这里就是ConcurrentHashMap锁的对象了,那这个f是什么?就是这个hash槽的头结点

ConcurrentHashMap为什么不能放null

二义性问题

如果支持了Null值的话,在多线程的场景下containsKey()为true后,调用get()方法返回为Null的话,就无法确定到底是因为这个key

的值本身就是Null,还是说在containsKey()到get()的方法的这段时间内,这个key被删掉了

问题来了,那为什么HashMap又可以放null?

因为HashMap设计的本身就是为了面对的是线程不安全的场景,默认是只考虑单线程的时候的情况

单线程不会出现多线程的问题,不会说在containsKey()到get()的方法的这段时间内,这个key被删掉了的这种情况

我看到有的人说,因为ConcurrentHashMap插入的过程中需要对头结点进行CAS或者加锁,key是null的话就加不了锁

这句话很明显就是错误的,如果这样理解了说明对Map还是不理解,Map底层就是Node<K,V>的数组[],你K为null,value为null

并不影响这个Node对象的创建,所以也不会存在key是null的话就加不了锁的情况

相关推荐
lizhongxuan几秒前
PgBackRest备份原理详解
后端
真是他1 分钟前
多继承出现的菱形继承问题
后端
Java技术小馆1 分钟前
SpringBoot中暗藏的设计模式
java·面试·架构
xiguolangzi2 分钟前
《springBoot3 中使用redis》
java
Aniugel3 分钟前
JavaScript高级面试题
javascript·设计模式·面试
李菠菜5 分钟前
POST请求的三种编码及SpringBoot处理详解
spring boot·后端
李菠菜6 分钟前
浅谈Maven依赖传递中的optional和provided
后端·maven
李菠菜9 分钟前
非SpringBoot环境下Jedis集群操作Redis实战指南
java·redis
lqstyle9 分钟前
Redis的Set:你以为我是青铜?其实我是百变星君!
后端·面试
Piper蛋窝13 分钟前
Go 1.15 相比 Go 1.14 有哪些值得注意的改动?
后端