JUC并发编程基础学习之HashMap线程安全问题

前言

在进行HashMap的线程安全测试前,我们先来思考几个问题,HashMap的正确使用方法是什么?你平时习惯的使用方式是否正确?如果不正确,那么正确的使用方式到底是什么呢?如果你对HashMap的使用存有疑虑,相信你在看完本篇博客后将会有所收获!

1.HashMap线程安全测试

1.1 HashMap的使用方式
1.1.1 习惯的使用方式

我们平常在使用HashMap时,通常的使用习惯可能如下:

//获取一个HashMap对象
Map<String,String> map = new HashMap<>();

但你认为这样是正确且合理的使用方法吗?

答案:并不是,因为在《阿里巴巴Java开发手册》中曾提到过,我们在使用HashMap时,应当在创建时指定其初始容量大小

那么,正确的使用方式是什么呢?请继续往下看

1.1.2 正确的使用方式

在使用HashMap之前,我们先简单查看一下HashMap构造函数的相关源码:

  • HashMap(int, float)型构造函数源码
    /**
     * 构造一个指定初始容量和负载因子的空的HashMap 
     *
     * @param  initialCapacity 初始容量(int型)
     * @param  loadFactor      负载因子(float型)
     * @throws 如果初始容量是负数或负载因子是负数, 抛出IllegalArgumentException(非法参数异常)
     */
    //HashMap的有参构造函数(包含两个参数:初始容量和负载因子)
    public HashMap(int initialCapacity, float loadFactor) {
        //判断初始容量是为负数
        if (initialCapacity < 0)
            //如果为负数, 抛出非法参数异常
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        //判断初始容量是否大于最大容量
        if (initialCapacity > MAXIMUM_CAPACITY)
            //如果大于,初始容量值设置为最大容量值
            initialCapacity = MAXIMUM_CAPACITY;
        //判断负载因子是否小于等于0 或者 负载因子是否为float类型
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            //如果负载因子为负数或负载因子不是float类型的,将会抛出非法参数异常
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        //初始化负载因子
        this.loadFactor = loadFactor;
        //初始化容量
        this.threshold = tableSizeFor(initialCapacity);
    }

在看到 Float.isNaN(loadFactor) 这个判断条件时,感到有些疑惑,然后去看了下其对应的源码:

  • Float类的isNaN方法源码
package java.lang;

//Float类
public final class Float extends Number implements Comparable<Float> {
    
    //...省略前面部分代码
    
    /**
     * 如果指定的数是一个Not-a-Number(NaN), 即不是一个数字,返回布尔值true, 否则返回false
     *
     * @param   v   测试值
     * @return  如果参数为NaN(非数字类型), 则返回布尔值true, 否则返回false
     */
    public static boolean isNaN(float v) {
        return (v != v);
    }
    
    //...省略后面部分代码
}

最后在看到 this.threshold = tableSizeFor(initialCapaity) 对于这个tableSizeFor又有些疑惑,查看其对应方法源码:

  • HashMap的tableSizeFor方法源码
    /**
     * 返回大于初始容量的最小的2次幂数值
     * @param cap 设置的初始容量
     */
	//初始化哈希表的大小
    static final int tableSizeFor(int cap) {
        //n应该是值哈希表中的元素个数, 其初始值为初始容量值减1
        int n = cap - 1;
        //执行无符号右移运算, 忽略符号位, 空位都以0补齐
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        /**
         * 这里的返回值是进行了两个三元运算操作比较:
         * 判断n(元素个数)是否小于0, 若元素个数小于0, 则返回值为1, 否则判断n(元素个数)是否大于等于最大容量, 若大于HashMap的最大容量, 则返回值为最大容量值, 否则返回值为n+1(元素个数加1)
         */ 
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

看完了tableSizeFor源码后,我们得知

在我们自定义HashMap的初始容量大小时, HashMap(int,float)构造函数并非直接使用我们所设置的容量值,而是调用了tableSizeFor方法, 把设置的容量值做为参数, 然后把该函数的返回值作为最后的初始容量大小!

HashMap(int,float)构造函数中,我们了解到,在创建HashMap时,应该设置一下初始容量和负载因子大小,接下来看一下正确的使用方式

  • 正确的使用方式如下所示
Map<String,String> map = new HashMap<>(16, 0.75F);

其中第一个参数是初始容量 (initialCapacity), 其默认值为16;而第二个参数是负载因子 (loadFactor), 其默认值为0.75F (F表示其为双精度浮点数,即为数据类型为float型)

那么请你再思考一个问题,在HashMap中的源码中,它的初始容量是直接用16表示吗?

答案:并不是,在HashMap源码中,关于默认初始容量大小表示如下:

//默认的初始容量 - 必须是2的幂数
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

这里初始容量大小使用了一个 1 << 4 表示形式,很多基础薄弱的小伙伴看到后就会心生疑惑,这个 << 到底是什么呢? 其实这是使用了位运算符来进行表示,这里简单介绍一下常用的三种位运算符:

<<左移运算符 >>右移运算符 >>>无符号右移

这里以左移运算符为例,左移的运算规则是: 按照二进制形式把所有数字做移动对应的位数, 高位移出(舍弃),低位的空位补零

知道了原理后,我们来计算一下 1 << 4 转换为 十进制数后是不是16呢?

根据左移运算的规则,那么1 << 4 表示的就是向左移动4位,使用二进制数表示就是 0001 0000 ,再转换为十进制数后,你会发现,这个值刚好等于16

那么,为什么JDK开发人员不直接使用16,而偏偏要使用看上去难懂一些的 1 << 4 呢?

我的理解是

这样做的目的是,在使用HashMap进行插入数据(即putValue操作)时,首先需要判断数组是否为空,若数组不为空,需要通过 (n-1) & hash 来计算插入元素应当存放在数组中的下标i,而使用1<<4 恰好可以将节点进行平均分配。

上面提到了HashMap中的putValue操作,很多小伙伴可能还不太明白,所以我们来看一下它的源码,顺便验证一下我上面的结论

  • HashMap的putValue方法源码

putValue方法的相关字段:

    //相关字段

    /**
     * 链表转化成红黑树的树化阈值 
     * 当添加元素时, 链表中的节点数至少有阈值大小, 会将链表转换为红黑树 
     * 为符合红黑树转化为链表的条件, 该阈值必须大于2且至少为8
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 红黑树转化为链表的链化阈值
     * 在转化时进行收缩检测, 节点数应小于该计数阈值, 且最多6个节点
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 转化成红黑树的最小数组容量
     * (否则, 若数组中节点太多, 则会调整表的大小(进行数组扩容))
     * 为避免调整大小和树化阈值之间的冲突, 该值至少是树化阈值的4倍
     */
    static final int MIN_TREEIFY_CAPACITY = 64;
    
	//...(中间省略部分代码)...

    /**
     * 存储哈希桶的数组
     * 第一次使用该数组时,会进行初始化, 并根据需要调整大小, 且当分配时, 数组长度总是2的幂数
     * (在某些操作中, 我们也允许长度为零, 以允许当前不需要的引导机制)
     */
    transient Node<K,V>[] table;

    /**
     * Map集合中包含键值对映射的数量
     */
    transient int size;

    /**
     * HashMap发生结构性修改的次数
     * 结构性修改是指那些HashMap中的映射数量改变, 或者修改其内部结构(例如rehash操作).
     * 该字段用于让HashMap的集合视图中迭代器(fast-fail)快速失效
     * (抛出ConcurrentModificationException(并发修改异常)).
     */
    transient int modCount;

    /**
     * 扩容阈值 (容量 * 负载因子).
     *
     * @serial
     */
    //如果哈希表的数组没有进行分配, 这个字段用于保存数组的初始容量, 或者0来表示DEFAULT_INITIAL_CAPACITY(默认初始容量)
    int threshold;

putValue方法源码:

    //putValue方法
    /**
     * 实现Map集合的put操作和相关方法
     *
     * @param hash  key的hash值
     * @param key 键值
     * @param value 存入的value值
     * @param onlyIfAbsent 如果值为true, 不改变已存在的值
     * @param evict 如果为false, 哈希表处于创建模式
     * @return 如果没有, 返回先前的值或者null值
     */
    //HashMap的put值操作的方法
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //tab是指table(即存储hash桶的数组), p是已存在Node节点, 而n是该数组的长度, i是数组下标
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //步骤一: 判断数组是否为空, 为空则初始化数组
        //1.判断数组是否为空 或者 数组长度是否为0
        if ((tab = table) == null || (n = tab.length) == 0)
            //如果满足数组为空或者其长度为0, 则初始化数组
            n = (tab = resize()).length;
        //步骤二: 判断数组是否为空, 不为空则计算存放下标位置
        //2.若数组不为空
        //2.1 判断数组下标是否为空(使用n-1 & hash 进行计算)
        if ((p = tab[i = (n - 1) & hash]) == null)
            //2.2 数组下标(i)为空(即当前下标中不存在数据), 则创建一个Node节点, 将其存放在数组中下标为i的位置
            tab[i] = newNode(hash, key, value, null);
        else {
            //步骤三: 判断数组下标是否为空, 不为空则下标位置存在数据, 发生哈希冲突 
            //3. 若数组下标不为空(即当前下标存在数据)
            //3.1 创建一个Node节点e, k是e的key值
            Node<K,V> e; K k;
            //3.2 判断是否发生哈希冲突(即存在两个hash值相同的节点)
            //3.2.1 判断p(已存在节点)的hash值和新建节点的hash值是否相等 并且 两者key值是否相等 或者 key值是否为空并且key值是否相等
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                //若p(已存在节点)和新建节点的hash值相等并且两者key值相同(即存在hash冲突), 则, 将p(已存在节点)的值赋给新建节点e
                e = p;
            //步骤五: 判断当前节点是否为树形节点, 若是则将新建节点加入红黑树中
            //前提: 若两者的hash值和key值不相同
            // 判断p(已存在节点)是否为树形节点
            else if (p instanceof TreeNode)
                //若p(已存在节点)是树形节点, 则将e(新建节点)加入到红黑树中
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //步骤六: 当前节点不为树形节点, 则新建Node节点到插入链表中去
            //前提: 若p(已存在节点)不是树形节点(即为链表节点)
            else {
                //6.使用尾插法在链表最末插入新节点
                for (int binCount = 0; ; ++binCount) {
                    //6.1 判断p(已存在节点)的下一个节点(即e新建节点)是否为空
                    if ((e = p.next) == null) {
                        //6.1.1 若p(已存在节点)的下一个节点为空, 则新建一个节点
                        p.next = newNode(hash, key, value, null);
                        //6.1.2 判断binCount(节点数)是否大于TREEIFY_THRESHOLD(树化阈值)减1(减1是第一个节点不计算在内)
                        if (binCount >= TREEIFY_THRESHOLD - 1) 
                            //#1 若节点数大于树化阈值, 则转化为红黑树
                            treeifyBin(tab, hash);
                        //#2 跳出循环
                        break;
                    }
                    //前提: 若p(已存在节点)的下一个节点不为空
                    //6.2 判断e(新建节点)和p(已存在节点)的key值和hash值是否相等
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        //6.2.1 若两个节点的key值和hash值相等, 则跳出循环    
                        break;
                   //6.2.2 若两个节点不同, 则将e(新建节点)赋值给p(已存在节点)
                    p = e;
                }
            }
            //步骤四: 两个节点的hash值和key值相同, 进行旧值覆盖  
            //4. 前提: 若p(已存在节点)和e(新建节点)的hash值和key值相同
            //4.1 判断e(新建节点)是否为空
            if (e != null) { 
                //4.1.1 覆盖旧值成功
                //#1 若e为不空, 则覆盖已存在节点p的旧值为新建节点e的新值
                V oldValue = e.value;
			   //#2 若e(新建节点)为空
                //判断onlyIfAbsent是否为false 或者 旧值是否为空
                if (!onlyIfAbsent || oldValue == null)
                    //4.1.2 覆盖旧值失败
                    //若onlyIfAbsent为false或者旧值为空, 则新建节点e的值还为新值value
                    e.value = value;
                //前提: 若e(新建节点不为空)
                //#3 e访问后调用
                afterNodeAccess(e);
                //#4 最后返回旧值
                return oldValue;
            }
        }
        //进行结构性修改
        ++modCount;
        //步骤七: 判断节点数是否大于阈值,大于就进行数组扩容
        //判断size(节点总数)是否大于threshold(扩容阈值)
        if (++size > threshold)
            //初始化数组(扩容为原来的两倍)
            resize();
        //节点插入后回调
        afterNodeInsertion(evict);
        return null;
    }
  • putValue操作流程图和步骤解释

流程图

在这里插入图片描述

步骤解释

  • 步骤一:首先判断数组是否为空,若数组为空,则将数组进行初始化
  • 步骤二:若数组不为空,通过 (n-1) & hash 计算存放在数组中的下标 i 位置
  • 步骤三:判断 tab [ i ] (数组中下标为 i 的位置) 是否存在数据,若不存在数据,则新建一个Node节点,将其存放在数组中的下标 i 位置中
  • 步骤四:若数组的下标 i 位置存在数据 ,说明发生了哈希冲突 (即已存在节点和插入节点的key值和hash值相同),判断两个节点的key是否相等 (使用equals方法进行比较),若key值相同,则用插入节点的value值覆盖掉已存在节点的value值 (前提是onlyAbsent方法的值为false)
  • 步骤五:若两个节点的key值不同,判断已存在节点是否为树形节点,若已存在节点为树形节点,则将新建节点插入到红黑树中
  • 步骤六:若已存在节点不是树形节点,则创建一个普通Node节点,将新建节点插入到链表尾部;然后判断链表中的节点数是否达到树化阈值 (即链表节点数大于8并且数组长度大于64),若大于等于该值,则将链表转换成红黑树
  • 步骤七:判断总节点数是否大于扩容阈值,若大于该阈值(threshold),则将当前数组进行扩容(扩容为原来的两倍)

上面我们提到,在使用HashMap时,需要指定它的初始容量大小,但很多小伙伴表示为什么要指定初始容量大小有些疑惑,那么接下来就来解释这个问题

1.1.3 为什么需要指定初始容量大小?

如果没有设置HashMap初始容量大小,随着元素数量不断增加,HashMap会进行多次扩容,而HashMap中每次扩容都要执行rehash操作 (即重新格式化哈希表),频繁的扩容是非常消耗性能的

上面提到了HashMap会随着元素数量增多,而进行多次扩容,那么元素数量究竟达到多少,HashMap才会进行扩容呢

HashMap扩容的触发条件如下

当哈希表中的元素数目超过扩容阈值, 哈希表将执行rehash操作 (即重建内部的数据结构),以至于哈希表的存储桶数量会变为大约原来的两倍。

    /**
     * 扩容阈值 (容量 * 负载因子).
     * @serial
     */
    //如果哈希表的数组没有进行分配, 这个字段用于保存数组的初始容量, 或者0来表示DEFAULT_INITIAL_CAPACITY(默认初始容量)
    int threshold;

注意:临界值的计算公式为:threshold = initialCapacity * loadFactor (其中threshold表示扩容阈值,initialCapacity表示初始容量大小,loadFactor表示装载因子大小)

1.3.4 如何合理设置HashMap的初始容量大小?

关于HashMap初始容量大小的设置,在《阿里巴巴Java开发手册》中也有说明:

initiaCapacity(初始容量) = ( size (需要存储的元素个数) / loadFactor(负载因子) ) + 1

注意:loadFactor (即负载因子) 默认值为0.75,如果暂时无法确定初始值的大小,请设置其为16(即默认值)

按照阿里巴巴开发手册中的公式,我们可以来做个计算:

假设我们需要存储的元素个数为10个,负载因子使用默认的0.75,套入公式就是 (10 / 0.75) + 1,结果约等于 14,也就是说,如果我们想要存储10个元素,只需要将初始容量大小设置为14即可,但是JDK文档中说明初始容量必须是2的幂数,14显然不是2的幂数,我们发现14刚好在2的3次幂和2的4次幂之间,按照只大不小原则,那初始容量的大小就为16

讨论了如何合理设置HashMap的初始容量后,我们再来思考一个问题,为什么说初始容量不易设置太高(或负载因子设置太低)?

下面是在JDK文档中的相关解释:

HashMap为基础的操作(例如get和put)提供了持续时间的性能,假设哈希(散列) 函数将元素正确的分散存储在桶中,集合视图的迭代所需要的时间应该和HashMap实例的容量(桶的数量)加上它的大小(key-value(键值对)映射的数量)成正比。

因此, 如果重视迭代性能不能设置初始容量太高(或者负载因子太低)

你可能还注意到了,在上面提到,负载因子的默认值为0.75,那么为什么负载因子默认值要设置为0.75呢?让我们来继续看一下JDK文档中的相关解释:

1.3.5 为什么负载因子默认为0.75?

在解释负载因子的默认值为什么是0.75前,我们首先需要知道什么是负载因子?

JDK文档中负载因子的定义

负载因子是在HashMap容量自动增加之前,用于衡量哈希表容量允许达到的满度

如果你觉得这样的解释还是不够直观,我们可以结合哈希表的负载因子计算公式来理解

α = N / M (其中α是指装载因子,N表示哈希表中已存入的元素数,M表示哈希地址空间大小)

注意:α越小,冲突可能性就越小;α越大,冲突的可能性就越大

假设元素数N固定,如果α值越小,空间大小M就越大,即哈希表中的空闲单元比例就越大,因此待插入的元素和已插入的元素发生冲突的可能性就越小,但是其空间利用率就越低

反之,α值越大,空间大小M值就越小,哈希表中的空闲单元比例就越小,因此待插入的元素和已插入的元素发生冲突的可能性就越大,但其空间利用率就越高

JDK文档中关于负载因子默认值设置为0.75的相关解释

作为一般规则, 默认的负载因子(0.75)在时间和空间开销上提供一个好的权衡,更高的值减少了空间开销,但会增加查找成本 (体现在HashMap类的大多数操作中, 包括get和put) 。

1.2 HashMap线程安全测试
1.2.1 测试代码
package com.kuang.unsafe;


import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @ClassName MapTest
 * @Description HashMap线程安全测试类
 * @Author 狂奔の蜗牛rz
 * @Date 2021/8/2
 */
public class MapTest {

    public static void main(String[] args) {

        //创建一个HashMap对象
        Map<String,String> map = new HashMap<>();

        //用循环模拟多线程环境
        for (int i = 0; i < 30; i++) {
            //创建线程
            new Thread(()->{
               //向map中放入值: key键对应当前线程的名字, value值对应从数组中从0到5的随机字符串
               map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5));
               //打印map集合
               System.out.println(map);
            //获取数组中下标i位置的字符串, 并且启动线程
            },String.valueOf(i)).start();
        }
    }

}
1.2.2 测试结果

在这里插入图片描述

结果抛出ConcurrentModificationException(并发修改异常)!

1.2.3 结果分析

由此可知:HashMap是线程不安全的,只能在单线程下使用它

那么如果在多线程情况下,我们仍然想使用HashMap,该怎么办呢?

2. 解决HashMap线程不安全

2.1 HashMap线程安全解决方案

JDK官方给出的解决方案是,如果想保证HashMap的线程安全:

  • 使用Collections集合工具类的synchronizedMap方法
Map m = Collections.synchronizedMap(new HashMap(...));
  • 使用ConcurrentHashMap类
Map m = new ConcurrentHashMap();
2.2 使用Collections集合工具类的synchronizedMap方法
2.2.1 测试代码
package com.kuang.unsafe;


import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @ClassName MapTest
 * @Description HashMap线程安全测试类
 * @Author 狂奔の蜗牛rz
 * @Date 2021/8/2
 */
public class MapTest {

    public static void main(String[] args) {

        //创建一个HashMap对象
//        Map<String,String> map = new HashMap<>();
        //方案一: 使用Collections集合工具类中的synchronizedMap方法
        Map<String,String> map = Collections.synchronizedMap(new HashMap<>());

        //用循环模拟多线程环境
        for (int i = 0; i < 30; i++) {
            //创建线程
            new Thread(()->{
               //向map中放入值: key键对应当前线程的名字, value值对应从数组中从0到5的随机字符串
               map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5));
               //打印map集合
               System.out.println(map);
            //获取数组中下标i位置的字符串, 并且启动线程
            },String.valueOf(i)).start();
        }
    }

}
2.2.2 测试结果

在这里插入图片描述

结果执行成功, 没有抛出并发修改异常!

虽然在使用了使用Collections集合工具类的synchronizedMap方法后保证了HashMap线程安全,但很多小伙伴对它为什么就能保证线程安全感到疑惑,接下来就我们一起来查看一下它的底层源码!

2.2.3 源码解析
  • Collections集合工具类的synchronizedMap方法源码
    /**
     * 返回一个依靠指定Map集合同步的(线程安全)的Map
     * 如果指定Map集合被序列化, 返回的Map也将被序列化
     * @param <K> Map集合中键(keys)的类
     * @param <V> Map集合中值(values)的类
     * @param  m 在一个同步Map集合中被包装的map
     * @return 一个指定map集合的同步视图
     */
    public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
        //返回值为创建一个SynchronizeMap同步Map集合类
        return new SynchronizedMap<>(m);
    }

    /**
     * @serial include
     */
    //SynchronizeMap: 同步Map集合类
    private static class SynchronizedMap<K,V>
        //实现Map<K,V>集合接口和序列化接口
        implements Map<K,V>, Serializable {
        //持续版本UID
        private static final long serialVersionUID = 1978198479659022715L;
           
        private final Map<K,V> m;     // 备份的map集合
        final Object      mutex;        // 同步资源对象
        
        /**
         * SynchronizedMap的有参构造函数
         * @param m Map集合
         */
        SynchronizedMap(Map<K,V> m) {
            //备份map集合要求不能为空  
            this.m = Objects.requireNonNull(m);
            //同步资源对象
            mutex = this;
        }
       
        /**
         * SynchronizedMap的有参构造函数
         * @param m Map集合
         * @param mutex 同步资源对象
         */
        SynchronizedMap(Map<K,V> m, Object mutex) {
            //初始化Map集合
            this.m = m;
            //初始化同步资源对象
            this.mutex = mutex;
        }
        
        //...中间省略部分代码
        
        /**
         * 获取key(键)的方法
         * @param key 键
         */
        public V get(Object key) {
            //使用synchronized(同步锁)修饰共享资源mutex
            synchronized (mutex) {
                //将Map集合中获取的的key键返回
                return m.get(key);
            }
        }
        
        /** 
         * 存放key-value(键值对)的方法 
         * @param key 键
         * @param value 值
         */
        public V put(K key, V value) {
            //使用synchronized(同步锁)修饰共享资源mutex
            synchronized (mutex) {
                //将Map集合中存放的的键值对返回
                return m.put(key, value);
            }
        }
        
       //...中间省略部分代码
        
       /** 
        * 若键值对已存在存放key-value(键值对)的方法 
        * @param key 键
        * @param value 值
        */ 
       @Override
        public V putIfAbsent(K key, V value) {
            //使用synchronized(同步锁)修饰共享资源mutex
            synchronized (mutex) {
                //将Map集合中存放的的键值对返回
                return m.putIfAbsent(key, value);
            }
        }
        
        //...后面省略部分代码

  • 为了保证串行访问, 通过返回的map完成对备份map的所有访问是至关重要的;当用户迭代任何集合视图时, 必须手动同步返回的map集合:

使用方式:

Map m = Collections.synchronizedMap(new HashMap());
...
Set s = m.keySet();  //不需要在同步代码块中
...
synchronized (m) {  // 同步共享资源m, 不是Set集合s!
//获取迭代器
Iterator i = s.iterator(); // 必须在同步代码块中
//进行迭代 
while (i.hasNext())
	foo(i.next());
}

注意不遵循此建议可能会导致不确定性行为!

2.3 使用ConcurrentHashMap类
2.3.1测试代码
package com.kuang.unsafe;


import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @ClassName MapTest
 * @Description HashMap线程安全测试类
 * @Author 狂奔の蜗牛rz
 * @Date 2021/8/2
 */
public class MapTest {

    public static void main(String[] args) {

        //创建一个HashMap对象
//        Map<String,String> map = new HashMap<>();
        //方案二: 使用ConcurrentHashMap
        Map<String,String> map = new ConcurrentHashMap<>();

        //用循环模拟多线程环境
        for (int i = 0; i < 30; i++) {
            //创建线程
            new Thread(()->{
               //向map中放入值: key键对应当前线程的名字, value值对应从数组中从0到5的随机字符串
               map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5));
               //打印map集合
               System.out.println(map);
            //获取数组中下标i位置的字符串, 并且启动线程
            },String.valueOf(i)).start();
        }
    }

}
2.3.2 测试结果

在这里插入图片描述

结果执行成功, 没有抛出并发修改异常!


限于篇幅长度,对HashMap和ConcurrentHashMap源码的解析将会在后续博客中继续更新!那么今天的有关HashMap线程安全问题的学习到这里就结束了,欢迎大家学习和讨论,点赞和收藏!


参考视频链接:https://www.bilibili.com/video/BV1B7411L7tE (B站UP主遇见狂神说的JUC并发编程基础)

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

狂奔の蜗牛rz

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值