Day113 ConcurrentHashMap底层原理

本文详细探讨了ConcurrentHashMap的并发安全原理,包括其与HashMap的区别,使用CAS和synchronized保证线程安全的方式,以及1.7到1.8版本的变化。在1.8中,ConcurrentHashMap取消了Segment分段锁,采用了更细粒度的锁,提高了并发性能。
摘要由CSDN通过智能技术生成

ConcurrentHashMap

前言:现在很多会涉及到高并发的应用场景,最常用的双列集合HashMap是线程不安全的,HashTable虽然线程安全但却效率太低,所以就在HashMap的基础上有了并发安全的ConcurrentHashMap。

  • 为什么HashMap线程不安全
  1. 多线程put操作,导致元素丢失。
  2. 多线程put操作后,get操作导致死循环。
  • 为什么HashTable线程安全:因为它的remove,put,get都做成了同步方法。
  • HashTable安全机制的缺点:只有一把锁,效率很低。比如只要有一个线程put,其他线程即使put在其他桶里,也不能进行put操作。
  • 改进1:能不能多几把锁?—— 于是就有了JDK7中的分段锁,但只要有锁就不可避免有等待和竞争,会降低效率。
  • 改进2:能不能尽量不用锁?——于是就有了JDK8中的用到的无锁算法CAS和部分加锁。
  • ConcurrentHashMap的总体特征
    • 底层数据结构与HashMap相同为 数组+链表+红黑树
    • 支持高并发的访问和更新,它是线程安全的
    • 检索操作不用加锁,get方法是非阻塞的
    • key和value都不允许为null

其他前置知识:

  • 线程安全三大特征
    • 原子性和事务的原子性一样,对于一个操作或者多个操作,要么都执行,要么都不执行。
    • 指令有序性是指,在我们编写的代码中,上下两个互不关联的语句不会被指令重排序。指令重排序是指处理器为了性能优化,在无关联的代码的执行是可能会和代码顺序不一致。比如说int i = 1;int j = 2;那么这两条语句的执行顺序可能会先执行int j = 2;
    • 线程可见性是指一个线程修改了某个变量,其他线程能马上知道。
  • volatile:vvolatile修饰的变量具有可见性与有序性,但不保证原子性。
  • Java内存模型:每个线程都需要从主内存中获得变量的值,获得数据之后会放入自己的工作内存进行操作。
  • CAS算法
    • 为什么: 并发的问题根本在于缓存,两个线程改同一个数据时,并不是改主内存中的这个数据,而是改分别的CPU中的缓存,所以会出现改到同一个值。
    • 是什么:Compare and swap,先比较与内存中是否相等,如果相等再替换
      • 三个操作数:内存值V;旧的预期值A;要修改的新值B
      • 当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做
    • 怎么做:USAFE类中的 CompareAndSwapXXX 相关方法

与HashMap的区别

与HashMap基本一样是 数组+链表+红黑树,以下主要说明不同点

  • Node节点的变化:value和next都用volatile修饰,保证并发的可见性。
  • TreeNode和TreeBin:在ConcurrentHashMap中不是直接存储TreeNode来实现的,而是用TreeBin来包装TreeNode来实现的。这里也是与HashMap之间比较大的区别。
  • ForwordingNode:扩容阶段使用
  • 构造方法
    • 多了一个 public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)
    • 初始化过程也有区别:
    public ConcurrentHashMap() {
   
    }

    public ConcurrentHashMap(int initialCapacity) {
   
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
        //如果ic设为32,hm和1.7的chm会初始为32,但1.8的chm会初始为64 
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        this.sizeCtl = cap;
    }    

    public ConcurrentHashMap(int initialCapacity, float loadFactor) {
   
        this(initialCapacity, loadFactor, 1);
    }    
    
     * @param concurrencyLevel the estimated number of concurrently
     * updating threads. The implementation may use this value as
     * a sizing hint.
     // 并发度:对并发线程数量的一个估计值
    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
   
        if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        if (initialCapacity < concurrencyLevel)   // Use at least as many bins
            initialCapacity = concurrencyLevel;   // as estimated threads
        long size = (long)(1.0 + (long)initialCapacity / loadFactor);
        int cap = (size >= (long)MAXIMUM_CAPACITY) ?
            MAXIMUM_CAPACITY : tableSizeFor((int)size);
        this.sizeCtl = cap;
    }

并发安全的原理

概述:ConcurrentHashMap取消了segment分段锁,而采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。

img
下面主要通过阅读put操作源码来分析是如果利用CAS和Synchronized进行高效的同步更新数据。

  • put流程总结
  1. 初始化:判断Node[]数组是否初始化,没有则进行初始化操作
  2. 索引与CAS添加节点:通过hash定位Node[]数组的索引坐标,是否有Node节点,如果没有则使用CAS进行添加(链表的头结点),添加失败则进入下次循环。分类型添加链表节点或树节点。
  3. 扩容检查:检查到内部正在扩容,如果正在扩容,就帮助它一块扩容。
  4. 锁住头结点:如果f!=null,则使用synchronized锁住f元素(链表/红黑二叉树的头元素)
  5. 判断链表长度:已经达到临界值8 就需要把链表转换为树结构。
  6. 与HashMap.put()不同点:用CAS添加节点,用synchronized锁住头结点,扩容方面多线程辅助扩容。
public V put(K key, V value) {
   
    return putVal(key, value, false);
}

    /** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
   
    //ConcurrentHashMap 不允许插入null键,HashMap允许插入一个null键
    if (key == null || value == null) throw new NullPointerException();
    //计算key的hash值
    int hash = spread(key.hashCode());
    int binCount = 0;
    //for循环的作用:因为更新元素是使用CAS机制更新,需要不断的失败重试,直到成功为止。
    for (Node<K,V>[] tab = table;<
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值