ConcurrentHashMap,HashMap详解

前言

谈到HashMap的线程安全问题就不得不聊聊ConcurrentHashMap,ConcurrentHashMap和HashMap在很多地方是类似的,比如底层都是数组+链表+红黑树、数组大小都是2的幂次方等…一些重复的知识点在这里就不细讲了。这篇文章主要会解决以下几个问题:

HashMap为什么多线程下会不安全
什么是CAS算法
ConcurrentHashMap是如何解决线程安全问题的
ConcurrentHashMap查找以及插入过程
其实ConcurrentHashMap相比HashMap复杂了许多,主要是因为会涉及到许多并发层面的知识点,比如CAS算法、volitale以及synchronized关键字等,本文会粗略介绍一下相关知识点,接下来我们先聊聊HashMap的线程安全问题以及为什么要使用ConcurrentHashMap。

1.HashMap为什么线程不安全

HashMap在并发环境下主要有以下几个问题:

死循环(JDK1.7)
在1.7版本,当扩容后生成新数组,在转移元素的过程中,使用的是头插法,也就是链表的顺序会翻转,当多个线程执行插入操作时可能会发生死循环。在1.8版本时将头插法改成了尾插法,解决了死循环的问题。

数据丢失
当两个线程同时插入元素时可能会发生数据被覆盖的情况

先看下源码

 if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);

当两个线程同时执行到以上代码时,发现没有发生哈希冲突,于是新建Node节点插入,这时先插入的节点会被后插入的节点覆盖,导致数据丢失。

那么有哪些解决方法呢?

<1>Hashtable
给所有方法加synchronized锁,非常低效,现在已经淘汰。
<2>Synchronized Map
Collections包提供的一个方法,会同步整个对象,也不推荐使用
<3>ConcurrentHashMap
尽管没有同步整个Map,但是它仍然是线程安全的,读操作非常快,而写操作则是通过加锁完成的,推荐使用

2.介绍下CAS算法,这也是ConcurrentHashMap实现线程安全的一个关键点。

CAS可以看做是乐观锁的一种实现方式,Java原子类中的递增操作就通过CAS自旋实现的。
CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。

CAS底层就是通过Unsafe类中的方法来实现的,如下所示:

unsafe.compareAndSwapInt(this, valueOffset, expect, update)

//获取tab数组的第i个node
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
//使用CAS尝试更新table[i]
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
//写入table[i]
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}

下面介绍一下各个参数

this:Unsafe对象本身,需要通过这个类来获取value的内存偏移地址。
valueOffset:value变量的内存偏移地址。
expect:期望更新的值。
update:要更新的最新值。
通过valueOffset可以拿到value的值,当且仅当value的值等于expect时,CAS通过原子方式用新值update来更新value的值,否则不会执行任何操作。

整个“比较+更新”操作封装在compareAndSwapInt()中,在JNI里是借助于一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。
ConcurrentHashMap的源码中除了普通的CAS操作,还定义了三个原子操作,用于对指定位置的节点进行操作。正是这些原子操作保证了ConcurrentHashMap的线程安全

3.ConcurrentHashMap

ConcurrentHashMap支持并发的读写。跟1.7版本相比,JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,虽然源码里面还保留了,也只是为了兼容性的考虑,因此本文主要讲解的是JDK1.8版本的ConcurrentHashMap。

private transient volatile int sizeCtl;
transient volatile Node<K,V>[] table;//哈希数组,保存Ndode节点
private transient volatile Node<K,V>[] nextTable;//扩容用的数组,只有在扩容时才不为null
private static final int DEFAULT_CAPACITY = 16;//默认大小
private static final float LOAD_FACTOR = 0.75f;//负载因子
static final int MOVED = -1; //表示正在扩容

介绍一个核心属性sizeCtl
用途:控制table数组的初始化和扩容的操作,不同的值有不同的含义
当为负数时:-1代表正在初始化,-N代表有N-1个线程正在进行扩容
当为0时(默认值):代表table数组还没有被初始化
当为正数时:表示初始化或者下一次进行扩容的大小

4.volatile关键字

在上面我们可以看到volatile关键字,这里先简单介绍一下该关键字的作用:

保证变量的可见性
在多线程环境下,某个共享变量如果被其中一个线程给修改了,其他线程能够立即知道这个共享变量已经被修改了,当其他线程要读取这个变量的时候,最终会去内存中读取,而不是从自己的工作空间中读取

保证有序性
虚拟机在进行代码编译优化的时候,对于那些改变顺序之后不会对最终变量的值造成影响的代码,是有可能将他们进行重排序的,但是在多线程下可能会引发线程安全问题,使用volatile可以禁止重排序。

注意:volatile关键字无法保证变量的原子性。

在面试中volatile底层实现机制也是常考的一个知识点,由于篇幅有限这里只是简单介绍一下概念,如果对原理感兴趣的同学可以上网搜索一下相关资料。

数据结构

ConcurrentHashMap和HashMap都是由数组+链表+红黑树构成,不过有一个不同的是ConcurrentHashMap的数组中放入的不是TreeNode结点,而是将TreeNode包装起来的TreeBin对象,如下图所示:
在这里插入图片描述

继续看我们的ConcurrentHashMap

构造方法

public ConcurrentHashMap() {
}

public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        this.sizeCtl = cap;
}

和HashMap实现差不多,也是用tableSizeFor方法来确保数组大小为2的幂次方, 可以看出构造函数主要是设定sizeCtl的值,并未对表进行初始化。当表未初始化的时候,sizeCtl的值其实指定的是表的长度。

初始化

在ConcurrentHashMap里table数组第一次初始化是在initTable里执行的,这点和HashMap有点不同,简单看下初始化步骤:

当数组table未初始化时,当 sizeCtl < 0 说明有别的线程正在初始化或扩容,自旋等待
接着尝试调用CAS去更新sizeCtl的值
若更新成功初始化table数组,并且把sizeCtl设置为容量阈值(也就是HashMap的threshold)
若更新失败则说明别的线程已经执行过初始化操作了,直接返回table数组即可

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    
    while ((tab = table) == null || tab.length == 0) {
        //当sizeCtl<0说明有别的线程正在初始化或扩容,自旋等待
        if ((sc = sizeCtl) < 0)
            Thread.yield();
         //SIZECTL:表示当前对象的内存偏移量,sc表示期望值,-1表示要替换的值,设定为-1表示要初始化表
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                //检查table数组是否已经被初始化
                if ((tab = table) == null || tab.length == 0) {
                    //若sc=0则设置默认容量16,否则设置为指定容量大小
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];//初始化数组
                    table = tab = nt;
                    sc = n - (n >>> 2);//n - (n >>> 2) = 0.75n,也就是说sc的值等于threshold
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值