面试题:HashMap线程不安全 ConcurrentHashMap为什么线程安全

这篇具有很好参考价值的文章主要介绍了面试题:HashMap线程不安全 ConcurrentHashMap为什么线程安全。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

背景

面试的时候先会喊你说说集合,那些集合线程不安全?当你说了HashMap线程不安全,面试官可能会进一步询问你是否了解 ConcurrentHashMap,以及它是如何实现线程安全的。

常见集合线程安全性

ArrayList、LinkedList、TreeSet、HashSet、HashMap、TreeMap等都是线程不安全的。

HashTable是线程安全的。

HashMap为什么线程不安全?

来看个例子

public static void main(String[] args) {
       HashMap<String, Integer> map = new HashMap<>();

       // 创建两个线程同时向HashMap中添加1000个元素
       Thread thread1 = new Thread(new Runnable() {
           @Override
           public void run() {
               for (int i = 0; i < 1000; i++) {
                   map.put(String.valueOf(i), i);
               }
           }
       });

       Thread thread2 = new Thread(new Runnable() {
           @Override
           public void run() {
               for (int i = 1000; i < 2000; i++) {
                   map.put(String.valueOf(i), i);
               }
           }
       });

       // 启动线程
       thread1.start();
       thread2.start();

       try {
           // 等待两个线程执行完成
           thread1.join();
           thread2.join();
       } catch (InterruptedException e) {
           e.printStackTrace();
       }

       // 输出 HashMap 的大小
       System.out.println("map集合大小: " + map.size());
   }
   //输出结果:map集合大小:1920(这个数字在变动)

在多线程环境下,如果多个线程同时HashMap 进行修改操作(例如添加、删除元素),可能会导致数据结构破坏,进而引发各种问题,比如丢失数据等。

怎么保证HashMap线程安全

第一
如何保证HashMap的线程安全呢?可能你们想到了synchronized ,确实,你可以通过在添加元素时使用 synchronized 来确保 HashMap 的线程安全性。这样做可以在多线程环境下保证对 HashMap 的操作是互斥的,从而避免了多个线程同时修改 HashMap 导致的线程不安全问题。

 Thread thread1 = new Thread(new Runnable() {
      @Override
      public void run() {
      	  //synchronized (map) 中的 map 是一个对象锁
      	  //它指定了在执行同步代码块时使用的锁对象
          synchronized (map) {
              for (int i = 0; i < 1000; i++) {
                  map.put(String.valueOf(i), i);
              }
          }
      }
  });

当一个线程进入同步代码块(即 synchronized (map) 所包围的部分)时,它会尝试获取 map 对象的锁。如果这个锁当前没有被其他线程占用,那么该线程将获得锁,并可以执行同步代码块中的操作如果该锁已经被其他线程占用,那么该线程将被阻塞,直到锁被释放。被锁住的对象将会在同步代码块执行完毕后自动释放。

第二
使用Collections.synchronizedMap() 包装:
Map<Integer, String> synchronizedMap = Collections.synchronizedMap(new HashMap<>());
这样可以获得一个线程安全的 HashMap,但性能可能不如 ConcurrentHashMap。

第三
还可以用其他锁ReentrantReadWriteLock来控制多线程下对集合安全读写。可能有人又疑惑好像自己见过ReentrantLock 。ReentrantLock 是一个可重入的互斥锁,而 ReentrantReadWriteLock 是一个可重入的读写锁。区别:

互斥性:
ReentrantLock 是一种独占锁,同一时刻只允许一个线程获得锁,其他线程必须等待该线程释放锁才能继续执行。
ReentrantReadWriteLock 包含了两种锁:读锁和写锁。多个线程可以同时持有读锁,但是只有一个线程可以持有写锁,并且在持有写锁时,不允许其他线程持有读锁或写锁。

读写分离:
ReentrantReadWriteLock 的设计目的是为了提高读操作的并发性能。在读多写少的情况下,读锁允许多个线程同时访问共享资源,从而提高了系统的并发能力。
ReentrantLock 没有读写分离的特性,它只是简单的互斥锁,适用于那些需要严格互斥的场景。

第四:
使用 ConcurrentHashMap:
ConcurrentHashMap 是专门为高并发环境设计的,JDK 1.8它使用了 CAS + synchronized 来保证线程安全性,而且性能表现优秀。
Map<Integer, String> concurrentHashMap = new ConcurrentHashMap<>();

ConcurrentHashMap为什么线程安全

ConcurrentHashMap 之所以是线程安全的,主要是因为它在内部实现时采用了特殊的机制来确保多个线程同时访问和修改数据时不会发生冲突。

JDK 1.7 版本中的实现:
ConcurrentHashMap 在 JDK 1.7 中使用了分段锁(Segmentation)的结构,将整个哈希表分成了多个段(Segment),每个段有自己的锁。不同段之间的修改操作可以并发进行(提高了并发性能),只有在同一段内的操作才需要竞争锁。

JDK 1.8 版本中的优化:
JDK 1.8 对 ConcurrentHashMap 进行了重大优化,废弃了分段锁的设计,而是采用了更细粒度的锁分离技术。
在 JDK 1.8 中,ConcurrentHashMap 内部使用了基于 CAS(Compare and Swap 是一种原子操作,用于在多线程环境下实现对共享数据的安全更新。CAS 是一种乐观锁机制,可以避免使用传统的互斥锁,提高了并发性能。) 操作的 synchronized 关键字
同时,ConcurrentHashMap 在 JDK 1.8 中引入了红黑树作为链表的替代结构,当链表长度达到一定阈值时,会将链表转换为红黑树,以提高查找效率。这种优化又提高了 ConcurrentHashMap 的并发性能和吞吐量。

代码中分析

来看看put方法

    public static  void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        map.put("a",111);
    }

先了解一下CAS
在 ConcurrentHashMap 中,CAS 主要用于对节点的插入更新删除操作。这些操作涉及到对节点的创建、替换和删除,需要确保线程安全,不能出现多线程操作同一节点的情况。使用 CAS 可以保证节点的创建、替换和删除的原子性,从而保证了线程安全。
在 JDK8 中,ConcurrentHashMap 的实现方式已经改变,不再采用分段锁的方式,而是采用了 CAS+Synchronized 的方式来保证线程安全

我们需要先了解tabAt ,casTabAt(本质是CAS 算法,看下面源码可知)的利用来保障线程安全的操作:
tabAt 方法用于从哈希表数组 tab 中获取指定索引 i 处的节点数组。
casTabAt 方法用于原子性地将哈希表数组 tab 中指定索引 i 处的值从 c 更新为 v。

    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);
    }

put源码如下:

public V put(K key, V value) {
        return putVal(key, value, false);
    }
  //下面会用到
  transient volatile Node<K,V>[] table;
 /** Implementation for put and putIfAbsent */
  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) {
          //如果tab为空,则尝试使用 casTabAt 方法原子地将新节点插入到tab中,如果插入成功则退出循环。
              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) {
              	  // 如果链表长度达到阈值(默认8),则将链表转化为红黑树以提高查找性能
                  if (binCount >= TREEIFY_THRESHOLD)
                      treeifyBin(tab, i);
                  if (oldVal != null)
                      return oldVal;
                  break;
              }
          }
      }
      addCount(1L, binCount);
      return null;
  }

注意其中table,synchronized , treeifyBin(tab, i);

table:
源码中赋值表达式Node<K,V>[] tab = table;,其中table是什么呢?定义table的源码: transient volatile Node<K,V>[] table;,其中使用了 transient 和 volatile 关键字进行修饰。
transient 关键字:表示该变量不会被序列化。在对象被序列化时,table 数组不会被保存,这意味着在反序列化时需要重新构建 table 数组。

volatile 关键字:表示该变量是易变的,并且在多线程环境下可能会被多个线程同时修改。当一个线程修改了 table 数组时,volatile 关键字可以确保对其他线程可见,即其他线程能够立即看到 table 数组的更新,而不会使用过期的或者缓存的值。

synchronized :
put方法里面其实调用了putVal。putVal中有 synchronized (f) {...}在链表中查找并插入新节点时,首先对桶进行加锁,然后遍历链表,查找对应的键值对。如果找到对应的键值对,则更新其值;如果未找到,则在链表末尾插入新节点。

treeifyBin(tab, i):
如果 binCount 大于等于预设的阈值 TREEIFY_THRESHOLD,则将链表转换为红黑树

小结

而在 JDK 1.8 中,ConcurrentHashMap 放弃了分段锁,而是采用了更为精细的桶结构。每个桶可以独立加锁,使得并发修改操作可以更细粒度地进行。此外,当桶中的元素数量达到一定阈值时,链表结构会转变为红黑树,以减少搜索时间。这种锁分离技术提高了并发性能,使得 ConcurrentHashMap 在并发情况下表现更加出色。它是通过 CAS + synchronized 来实现线程安全的,并且它的锁粒度更小,查询性能也更高。( JDK 1.7 中,ConcurrentHashMap 使用了分段锁的结构,即将整个哈希表分成多个段(Segment),每个段有自己的锁。这样,多个修改操作可以并发进行,只要它们发生在不同的段上,相应的段才会加锁。这种设计减少了不同线程之间的竞争,提高了并发性能。)


昨天面试被问到了,当时回答支支吾吾,今天总结了一下,可能有不到位的。欢迎大家的指点

觉得有用点个赞文章来源地址https://www.toymoban.com/news/detail-856039.html

到了这里,关于面试题:HashMap线程不安全 ConcurrentHashMap为什么线程安全的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

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

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

相关文章

  • Java面试题:为什么HashMap不建议使用对象作为Key?

    HashMap 是一种基于哈希表的动态数据结构,它允许使用任意不可变对象作为键(key)来存储和检索数据。然而,在某些情况下,使用对象作为 HashMap 的键可能会遇到一些问题。   首先,我们需要明确对象作为 HashMap 的键需要满足一些条件: 不可变性:对象的属性不能被修改,

    2024年04月22日
    浏览(33)
  • 【多线程进阶】信号量,线程安全集合类,Hashtable与ConcurrentHashMap的区别,多线程常见的面试题

    前言: 大家好,我是 良辰丫 ,今天学习多线程最后一节内容,我们主要去了解信号量,线程安全集合类,Hashtable与ConcurrentHashMap的区别,多线程常见的面试题,我们需要重点去掌握,💞💞💞 🧑个人主页:良辰针不戳 📖所属专栏:javaEE初阶 🍎励志语句:生活也许会让我们遍体鳞伤,

    2023年04月27日
    浏览(38)
  • java八股文面试[多线程]——为什么要用线程池、线程池参数

     速记7个: 核心、最大 存活2 队列 工厂 拒绝 线程池处理流程: 线程池底层工作原理: 线程复用原理:   知识来源: 【并发与线程】为什么使用线程池,参数解释_哔哩哔哩_bilibili 【并发与线程】线程池处理流程_哔哩哔哩_bilibili 【并发与线程】线程池的底层工作原理_哔哩

    2024年02月11日
    浏览(34)
  • ArrayList为什么不是线程安全的,如何保证线程安全?

    官方曰, 线程安全就是多线程访问时,采⽤了加锁机制,当⼀个线程访问该类的某个数据时,进⾏保护,其他线程不能进⾏访问直到该线程读取完,其他线程才可使⽤。不会出现数据不⼀致或者数据污染。线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数

    2024年02月07日
    浏览(48)
  • 为什么arrayList线程不安全?

            ArrayList是Java中的一种动态数组,它在内部使用数组来存储元素。ArrayList的线程不安全性主要体现在多线程并发访问和修改同一个ArrayList实例时可能出现的问题。         当多个线程同时对ArrayList进行修改操作时,可能会导致数据不一致或者出现异常。这是因为

    2024年02月12日
    浏览(33)
  • 美团面试拷打:ConcurrentHashMap 为何不能插入 null?HashMap 为何可以?

    周末的时候,有一位小伙伴提了一些关于 ConcurrentHashMap 的问题,都是他最近面试遇到的。原提问如下(星球原贴地址:https://t.zsxq.com/11jcuezQs ): 整个提问看着非常复杂,其实归纳来说就是两个问题: ConcurrentHashMap 为什么 key 和 value 不能为 null? ConcurrentHashMap 能保证复合操

    2024年02月11日
    浏览(24)
  • SimpleDateFormat为什么是线程不安全的?

    大家好,我是哪吒。 在日常开发中,Date工具类使用频率相对较高,大家通常都会这样写: 这很简单啊,有什么争议吗? 你应该听过“时区”这个名词,大家也都知道,相同时刻不同时区的时间是不一样的。 因此在使用时间时,一定要给出时区信息。 对于当前的上海时区和

    2024年02月20日
    浏览(27)
  • 为什么 ConcurrentHashMap 中 key 不允许为 null

    在ConcurrentHashMap 的源码,在 put 方法里面,可以看到这样一段代码,如果 key 或者 value 为空,则抛出空指针异常。 但是为什么 ConcurrentHashMap 不允许 key 或者 value 为空呢?   简单来说,就是为了避免在多线程环境下出现歧义问题。所谓歧义问题,就是如果 key 或者 value 为 nul

    2024年02月07日
    浏览(32)
  • 再谈StringBuilder为什么线程不安全以及带来的问题

    比较有意思的是,学习锁消除的过程中,有人讲到StringBuffer在方法内构建,不会被其他方法引用时,StringBuffer的锁会被消除, 于是,顺便看了一下同源的StringBuidler为什么线程不安全,以及为什么多线程不安全,和带来的问题, 有了这篇文章,分享出来,帮助读者轻松应对知

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

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

    2024年01月17日
    浏览(26)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包