• HashMap和ConcurrentHashMap区别


    HashMap是线程不安全的,而线程安全类Hashtable只是简单的在方法上加锁实现线程安全,效率低下,所以通常使用ConcurrentHashMap

    ConcurrentHashMap不允许空键值对,否则异常;

    1.ConcurrentHashMap怎样做到线程安全的?

    可以通过减少锁竞争来优化并发性能,而ConcurrentHashMap则在JDK8-使用了锁分段(减小锁范围)、JDK8开始大量使用CAS(乐观锁,减小上下文切换开销,无阻塞)和少量的同步代码块技术

    Jdk1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)

    Segment本身是基于ReentrantLock实现的加锁和释放锁的操作,这样就能保证多个线程同时访问ConcurrentHashMap时,同一时间只有一个线程能操作相应的节点,这样就保证了ConcurrentHashMap的线程安全了。也就是说ConcurrentHashMap的线程安全是建立在Segment 加锁的基础上的,所以我们把它称之为分段锁。

    总结

    ConcurrentHashMap 在 JDK 1.7 时使用的是数据加链表的形式实现的,其中数组分为两类:大数组 Segment 和小数组 HashEntry,而加锁是通过给 Segment 添加 ReentrantLock 锁来实现线程安全的。而 JDK 1.8 中 ConcurrentHashMap 使用的是数组+链表/红黑树的方式实现的,它是通过 CAS 或 synchronized 来实现线程安全的,并且它的锁粒度更小,查询性能也更高。

    节点类型

    Node节点

    1. static class Node implements Map.Entry{
    2. final int hash;
    3. final K key;
    4. volatile V val;
    5. volatile Node next;
    6. }

    默认桶上的节点就是Node结点。Node只有一个next指针,是一个单链表,提供find方法实现链表查询

    当出现Hash冲突时,Node节点会首先以链表的形式链接到table上,当结点数量超过一定数目时,链表会转化为红黑树。

    TreeNode节点

    1. static final class TreeNode extends Node {
    2. TreeNode root;
    3. volatile TreeNode first;
    4. volatile Thread waiter;
    5. volatile int lockState;
    6. static final int WRITER=1
    7. static final int WAITER=2;
    8. static final int READER=4;
    9. }

    TreeBin 会直接链接到table[i] 一桶上面,该节点提供了一系列红黑树相关的操作,以及加锁、解锁操作。

    另外TreeBin提供了一系列的操作

    TreeBin(TreeNode b),将以b为头结点的链表转换为红黑树

    lockRoot(),对红黑树的根节点加写锁

    unlockRoot(),释放写锁

    find(int h,Object k), 从根结点开始遍历查找,找到相等的节点就返回它,没找到就返回null,当存在写锁时,以链表方式进行查找,不阻塞读锁。

    ForwardingNode

    1. static final class ForwardingNode extends Node{
    2. final Node[] nextTable;
    3. }

    ForwardingNode在table扩容时使用,内部记录了扩容后的table,即nextTable中,在原table 的槽内放置一个ForwardingNode

    ForwardingNode是一种临时节点,在扩容进行中才会出现,hash值固定为-1,且不存储实际数据

    如果旧table数组的一个hash桶中全部的结点都迁移到了新table中,则在这个桶中放置一个ForwardingNode

    ForwardingNode是一种临时结点,在扩容进行中才会出现,hash值固定为-1,且不存储实际数据如果旧table数组的一个hash桶中全部的结点都迁移到了新table中,则在这个桶中放置一个ForwardingNode读操作碰到ForwardingNode时,将操作转发到扩容后的新table数组上去执行;写操作碰见它时,则尝试帮助扩容,扩容是支持多线程一起扩容。

    ReservationNode保留结点

    1. static final class ReservationNode extends Node{
    2. ReservationNode(){
    3. super(RESERVED,null,null);
    4. }
    5. Node find(int h,Object k){
    6. return null;
    7. }}

    在并发场景下、在从Key不存在到插入的时间间隔内,为了防止哈希槽被其他线程抢占,当前线程会使用一个reservationNode节点放到槽中并加锁,从而保证线程安全

    hash值固定为-3,不保存实际数据。只在computeIfAbsent和compute这两个函数式API中充当占位符加锁使用

    总结:

    就算有多个线程同时进行put操作,在初始化数组时使用了乐观锁CAS操作来决定 到底是哪个线程有资格进行初始化,其他线程均只能等待。

    用到的并发技巧:

    • volatile变量(sizeCtl):它是一个标记位,用来告诉其他线程这个坑位有没有人在,其线程间的可见性由volatile保证。

    • CAS操作:CAS操作保证了设置sizeCtl标记位的原子性,保证了只有一个线程能设置成功

  • 相关阅读:
    java毕业设计车位管理系统Mybatis+系统+数据库+调试部署
    C - 不定参数
    【斯坦福计网CS144项目】Lab3: TCPSender
    通过实例学C#之序列化与反序列化XmlSerializer类
    VS2010配置gdal1.10.0 gdal1.10.1编译
    JS高级-语言特性(持续更新一)
    【魔店】拼多多店铺一般在哪里找货源?
    CSS布局概念与技术教程
    Vue2.0和Vue3.0的区别
    艾美捷ProSci丨ProSci 40S核糖体蛋白S19重组蛋白介绍
  • 原文地址:https://blog.csdn.net/Lxcjl/article/details/126859069