ConcurrentHashMap原理详解(太细了)

这篇具有很好参考价值的文章主要介绍了ConcurrentHashMap原理详解(太细了)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、什么是ConcurrentHashMap

ConcurrentHashMapHashMap一样,是一个存放键值对的容器。使用hash算法来获取值的地址,因此时间复杂度是O(1)。查询非常快。
同时,ConcurrentHashMap是线程安全的HashMap。专门用于多线程环境。

二、ConcurrentHashMap和HashMap以及Hashtable的区别

2.1 HashMap

HashMap是线程不安全的,因为HashMap中操作都没有加锁,因此在多线程环境下会导致数据覆盖之类的问题,所以,在多线程中使用HashMap是会抛出异常的。

2.2 HashTable

HashTable是线程安全的,但是HashTable只是单纯的在put()方法上加上synchronized。保证插入时阻塞其他线程的插入操作。虽然安全,但因为设计简单,所以性能低下。

2.3 ConcurrentHashMap

ConcurrentHashMap是线程安全的,ConcurrentHashMap并非锁住整个方法,而是通过原子操作和局部加锁的方法保证了多线程的线程安全,且尽可能减少了性能损耗。

由此可见,HashTable可真是一无是处…

三、ConcurrentHashMap原理

这一节专门介绍ConcurrentHashMap是如何保证线程安全的。如果想详细了解ConcurrentHashMap的数据结构,请参考HashMap

3.1 volatile修饰的节点数组

请看源码

//ConcurrentHashMap使用volatile修饰节点数组,保证其可见性,禁止指令重排。
transient volatile Node<K,V>[] table;

再看看HashMap是怎么做的

//HashMap没有用volatile修饰节点数组。
transient Node<K,V>[] table;

显然,HashMap并不是为多线程环境设计的。

3.2 ConcurrentHashMap的put()方法
//put()方法直接调用putVal()方法
public V put(K key, V value) {
 return putVal(key, value, false);
}
//所以直接看putVal()方法。
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;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        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, null);
                                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;
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

我来给大家讲解一下步骤把。

public V put(K key, V value) {

首先,put()方法是没有用synchronized修饰的。

for (Node<K,V>[] tab = table;;)

新插入一个节点时,首先会进入一个死循环
情商高的就会说,这是一个乐观锁
进入乐观锁后,

if (tab == null || (n = tab.length) == 0)
    tab = initTable();

如果tab未被初始化,则先将tab初始化。此时,这轮循环结束,因为被乐观锁锁住,开始下一轮循环。
第二轮循环,此时tab已经被初始化了,所以跳过。

else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    if (casTabAt(tab, i, null,
                 new Node<K,V>(hash, key, value, null)))
        break;                   // no lock when adding to empty bin
}

接下来通过keyhash值来判断table中是否存在相同的key,如果不存在,执行casTabAt()方法。
注意,这个操作时不加锁的,看到里面的那行注释了么// no lock when adding to empty bin。位置为空时不加锁。
这里其实是利用了一个CAS操作。

CAS(Compare-And-Swap):比较并交换

这里就插播一个小知识,CAS就是通过一个原子操作,用预期值去和实际值做对比,如果实际值和预期相同,则做更新操作。
如果预期值和实际不同,我们就认为,其他线程更新了这个值,此时不做更新操作。
而且这整个流程是原子性的,所以只要实际值和预期值相同,就能保证这次更新不会被其他线程影响。

好了,我们继续。
既然这里用了CAS操作去更新值,那么就存在两者情况。

  1. 实际值和预期值相同
    相同时,直接将值插入,因为此时是线程安全的。好了,这时插入操作完成。使用break;跳出了乐观锁。循环结束。
  2. 实际值和预期值不同
    不同时,不进行操作,因为此时这个值已经被其他线程修改过了,此时这轮操作就结束了,因为还被乐观锁锁住,进入第三轮循环。

第三轮循环中,前面的判断又会重新执行一次,我就跳过不说了,进入后面的判断。

 else if ((fh = f.hash) == MOVED)
    tab = helpTransfer(tab, f);

这里判断的是tab的状态,MOVED表示在扩容中,如果在扩容中,帮助其扩容。帮助完了后就会进行第四轮循环。
终于,来到了最后一轮循环。

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, null);
                        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;
                }
            }
        }
    }
    if (binCount != 0) {
        if (binCount >= TREEIFY_THRESHOLD)
            treeifyBin(tab, i);
        if (oldVal != null)
            return oldVal;
        break;
    }
}

上面的判断都不满足时,就会进入最后的分支,这条分支表示,keyhash值位置不为null(之前的判断是hash值为null时直接做插入操作),表示发生了hash冲突,此时节点就要通过链表的形式存储这个插入的新值。Node类是有next字段的,用来指向链表的下一个位置,新节点就往这插。

synchronized (f) {

看,终于加排它锁了,只有在发生hash冲突的时候才加了排它锁。

if (tabAt(tab, i) == f) {
	if (fh >= 0) {

重新判断当前节点是不是第二轮判断过的节点,如果不是,表示节点被其他线程改过了,进入下一轮循环,
如果是,再次判断是否在扩容中,如果是,进入下一轮循环,
如果不是,其他线程没改过,继续走,

for (Node<K,V> e = f;; ++binCount) {

for循环,循环遍历这个节点上的链表,

if (e.hash == hash &&
    ((ek = e.key) == key ||
     (ek != null && key.equals(ek)))) {
    oldVal = e.val;
    if (!onlyIfAbsent)
        e.val = value;
    break;
}

找到一个hash值相同,且key也完全相同的节点,更新这个节点。
如果找不到

if ((e = e.next) == null) {
	pred.next = new Node<K,V>(hash, key,
                              value, null);
    break;
}

往链表最后插入这个新节点。因为在排他锁中,这些操作都可以直接操作。终于到这插入就基本完成了。

总结

做插入操作时,首先进入乐观锁,
然后,在乐观锁中判断容器是否初始化,
如果没初始化则初始化容器,
如果已经初始化,则判断该hash位置的节点是否为空,如果为空,则通过CAS操作进行插入。
如果该节点不为空,再判断容器是否在扩容中,如果在扩容,则帮助其扩容。
如果没有扩容,则进行最后一步,先加锁,然后找到hash值相同的那个节点(hash冲突),
循环判断这个节点上的链表,决定做覆盖操作还是插入操作。
循环结束,插入完毕。

3.3 ConcurrentHashMap的get()方法

//ConcurrentHashMap的get()方法是不加锁的,方法内部也没加锁。
public V get(Object key)

看上面这代码,ConcurrentHashMapget()方法是不加锁的,为什么可以不加锁?因为tablevolatile关键字修饰,保证每次获取值都是最新的。

//Hashtable的get()是加锁的,所以性能差。
public synchronized V get(Object key) 

再看看Hashtable,差距啊。

四、使用场景

嗯,多线程环境下,更新少,查询多时使用的话,性能比较高。
乐观锁嘛,认为更新操作时不会被其他线程影响。所以时候再更新少的情况下性能高。

对你有帮助吗?点个赞吧~文章来源地址https://www.toymoban.com/news/detail-779362.html

到了这里,关于ConcurrentHashMap原理详解(太细了)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • ConcurrentHashMap 底层原理

    tip:作为程序员一定学习编程之道,一定要对代码的编写有追求,不能实现就完事了。我们应该让自己写的代码更加优雅,即使这会费时费力。 推荐:体系化学习Java(Java面试专题) ConcurrentHashMap 是线程安全的哈希表,它是 Java 并发包中提供的一种高效的并发 Map 实现。Con

    2024年02月14日
    浏览(32)
  • Java-集合-ConcurrentHashMap

    table:数组加volatile保证可见性和有序性 put():数组不存在,通过CAS创建;数组下标位置为空,通过CAS插入;数组下标位置不为空,给头节点加synchronized来插入链表或红黑树 ConcurrentHashMap是通过synchronized保证线程安全的吗? 不是,HashTable是单纯给方法加synchronized来保证单机线

    2024年02月10日
    浏览(75)
  • Java-多线程-深入理解ConcurrentHashMap

        ConcurrentHashMap(Concurrent: 并存的,同时发生的 ;)     ConcurrentHashMap是Java中的一个线程安全的哈希表实现,它可以在多线程环境下高效地进行并发操作。     HashMap线程不安全,在多线程操作下可能会导致数据错乱     使用HashMap和ConcurrentHashMap分别实

    2024年02月14日
    浏览(38)
  • Java进阶(ConcurrentHashMap)——面试时ConcurrentHashMap常见问题解读 & 结合源码分析 & 多线程CAS比较并交换 初识

    List、Set、HashMap作为Java中常用的集合,需要深入认识其原理和特性。 本篇博客介绍常见的关于Java中线程安全的ConcurrentHashMap集合的面试问题,结合源码分析题目背后的知识点。 关于List的博客文章如下: Java进阶(List)——面试时List常见问题解读 结合源码分析 关于的Set的博

    2024年02月06日
    浏览(61)
  • 【JAVA】concurrentHashMap和HashTable有什么区别

    🍎 个人博客: 个人主页 🏆 个人专栏: JAVA ⛳️   功不唐捐,玉汝于成 目录 前言 正文 同步性质: 性能: 允许空键值(Allow Nulls): 迭代器(Iterator): 继承关系: 结语  我的其他博客 在Java的集合框架中, ConcurrentHashMap 和 HashTable 都提供了线程安全的哈希表实现,用

    2024年01月16日
    浏览(54)
  • 【Java】HashMap、HashTable和ConcurrentHashMap的区别

    HashTable、HashMap和ConcurrentHashMap之间的区别主要体现在线程安全、继承关系与实现接口、对null值的处理、性能以及数据结构等几个方面。以下是对这三者之间区别的详细分析: 项目 HashMap HashTable ConcurrentHashMap null键 允许(仅能有一个) 不允许 允许(仅能有一个) null值 允许

    2024年04月25日
    浏览(57)
  • 淘宝太细了:mysql 和 es 的5个一致性方案,你知道吗?

    在40岁老架构师 尼恩的 读者交流群 (50+)中,最近有小伙伴拿到了一线互联网企业如拼多多、极兔、有赞、希音的面试资格,遇到一几个很重要的面试题: 说5种mysql 和 elasticsearch 数据一致性方案 与之类似的、其他小伙伴遇到过的问题还有: Mysql 和 ES 数据一致性问题及方案?

    2024年02月13日
    浏览(35)
  • 【JAVA】Java8开始ConcurrentHashMap,为什么舍弃分段锁

    🍎 个人博客: 个人主页 🏆 个人专栏:      JAVA    ⛳️  功不唐捐,玉汝于成 目录 前言  正文 分段锁的好处: 结语 我的其他博客 前言  在Java 8中, ConcurrentHashMap 的实现经历了重大的改进,其中最引人注目的变化之一就是舍弃了传统的分段锁机制,转而采用了基于C

    2024年01月17日
    浏览(38)
  • 常用Java代码-Java中的并发集合(ConcurrentHashMap、CopyOnWriteArrayList等)

    在Java中,并发集合是一组为多线程环境设计的集合类,它们提供了线程安全的操作。这些集合类包括 ConcurrentHashMap , CopyOnWriteArrayList 等。以下是对这两个类的一个简单的代码解释。 1.ConcurrentHashMap ConcurrentHashMap 是Java并发包 java.util.concurrent 中的一个类,它提供了线程安全的

    2024年01月21日
    浏览(42)
  • 大聪明教你学Java | 深入浅出聊 ConcurrentHashMap

    🍊作者简介: 不肯过江东丶,一个来自二线城市的程序员,致力于用“猥琐”办法解决繁琐问题,让复杂的问题变得通俗易懂。 🍊支持作者: 点赞👍、关注💖、留言💌~ 在 Java 的集合框架中,HashMap 是一种非常常用的数据结构,它提供了键值对形式的存储和访问方式。然

    2024年02月10日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包