重新认识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的话就加不了锁的情况

相关推荐
徐*红3 分钟前
java 线程池
java·开发语言
尚学教辅学习资料3 分钟前
基于SSM的养老院管理系统+LW示例参考
java·开发语言·java毕设·养老院
2401_857636393 分钟前
计算机课程管理平台:Spring Boot与工程认证的结合
java·spring boot·后端
1 9 J5 分钟前
Java 上机实践4(类与对象)
java·开发语言·算法
Code apprenticeship6 分钟前
Java面试题(2)
java·开发语言
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
憨子周1 小时前
2M的带宽怎么怎么设置tcp滑动窗口以及连接池
java·网络·网络协议·tcp/ip
霖雨3 小时前
使用Visual Studio Code 快速新建Net项目
java·ide·windows·vscode·编辑器
SRY122404193 小时前
javaSE面试题
java·开发语言·面试
Fiercezm3 小时前
JUC学习
java