首页 > 其他分享 >HashMap线程不安全|Hashtable|ConcurrentHashMap

HashMap线程不安全|Hashtable|ConcurrentHashMap

时间:2024-09-12 16:50:56浏览次数:13  
标签:map ConcurrentHashMap 加锁 HashMap 线程 new

文章目录

常见集合线程安全性

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。

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


Hashtable

Hashtable 使用的是全表加锁的方式来保证线程安全,也就是说,当一个线程要对 Hashtable 进行读写操作时,它会对整个 Hashtable 加锁,阻塞其他所有线程的访问
在这里插入图片描述
HashTable 替代品 ConcurrentHashMap ,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);
    }

在 JDK8 中,ConcurrentHashMap 的实现方式已经改变,不再采用分段锁的方式,而是采用了 CAS+Synchronized 的方式来保证线程安全

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

put源码分析:

  1. 遍历桶(Bin)时不加锁
    首先,代码会根据 hash 值找到对应的桶(tab[i]),并且如果该桶是空的(f == null),会尝试通过 CAS(compare-and-swap) 操作将新的节点放入其中,这一部分操作是无锁的。

    else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
        if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
            break;
    }
    
  2. 对非空桶的头节点加锁
    当桶 tab[i] 已经存在节点时,会进入 synchronized (f) 代码块,其中 f 是桶中的头节点。通过对头节点 f 加锁,保证对该桶中的修改是线程安全的。

    synchronized (f) {
        if (tabAt(tab, i) == f) {
            // 锁定成功后,进行进一步操作
        }
    }
    
    • 这种加锁方式是“桶级别”的锁,而不是对整个 HashMapConcurrentHashMap 加全局锁,因此不会影响其他线程对其他桶的访问。

在这里插入图片描述

  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;
          }
      }
      
    • 如果该桶是树形结构(红黑树),则调用 putTreeVal 方法进行树中的插入操作:

      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;
          }
      }
      
  2. 加锁粒度
    由于只对每个桶的头节点加锁,因此即使多个线程同时对不同的桶进行操作,也不会相互阻塞。只有在同一个桶中有多个线程试图修改数据时,才会发生锁竞争。这种设计提高了并发性能,相比全局锁的方式有更高的并发吞吐量。

  3. 树化操作
    当链表中的节点数量超过 TREEIFY_THRESHOLD 时,会将链表转化为红黑树以提高查询和修改效率。树化也是在线程安全的环境下进行的,但依然是通过对头节点加锁实现的。

    if (binCount >= TREEIFY_THRESHOLD)
        treeifyBin(tab, i);
    

总结

  • 加锁机制:对每个桶(tab[i])的头节点进行加锁。这种锁定策略使得同一个桶中的修改是线程安全的。
  • 并发性能优化:这种设计避免了全局锁,提高了在高并发环境下的性能。当多个线程操作不同的桶时,操作不会相互干扰。

小结

而在 JDK 1.8 中,ConcurrentHashMap 放弃了分段锁,而是采用了更为精细的桶结构。每个桶可以独立加锁,使得并发修改操作可以更细粒度地进行。此外,当桶中的元素数量达到一定阈值时,链表结构会转变为红黑树,以减少搜索时间。这种锁分离技术提高了并发性能,使得 ConcurrentHashMap 在并发情况下表现更加出色。它是通过 CAS + synchronized 来实现线程安全的,并且它的锁粒度更小,查询性能也更高。


❤觉得有用的可以留个关注ya❤

标签:map,ConcurrentHashMap,加锁,HashMap,线程,new
From: https://blog.csdn.net/m0_64289188/article/details/142180656

相关文章

  • Linux环境C语言pthread多线程
    pthread线程库介绍pthread库是POSIX线程(PortableOperatingSystemInterfaceforuniXthreads)库的简称,它提供了一套创建和管理线程、以及线程间同步的机制。pthread库是UNIX系统上实现多线程编程的一个标准接口,也被广泛支持在类UNIX系统(Linux和macOS)中。头文件#in......
  • 视频监控推流助手/极低延迟/支持N路批量多线程推流/264和265推流/监控转网页
    一、前言说明搞视频监控开发除了基本的拉流以外,还有个需求是推流,需要将拉到的流重新推流到流媒体服务器,让流媒体服务做转发和负载均衡,这样其他地方只需要问流媒体服务器要视频流即可。为什么拉了又重新推呢,因为软件这边和可能拉流后做了处理,比如做了人工智能运算,识别到了物体方框......
  • 【Linux 19】线程概念
    文章目录......
  • HashMap源码分析
    HashMap源码分析在jdk1.8中,HashMap的数据结构如上图所示,是由Node数组+链表/红黑树组成的,每个K-V对保存在一个Node结点中,看一下Node结点的定义,其实就是一个Map.Entry<K,V>的实现类,包括key的hash值,key,value和一个next指针。staticclassNode<K,V>implementsMap.Entry<K,V>{......
  • java使用多线程
    importjava.util.concurrent.TimeUnit;importcn.hutool.core.thread.ExecutorBuilder;importcn.hutool.core.thread.ThreadFactoryBuilder;//构造多线程,可修改线程数ExecutorServiceexecutorService=ExecutorBuilder.create().setCorePoolSize(5)//初始线程......
  • JAVA线程基础——ThreadLocal的使用和原理
    一、ThreadLocal        多线程访问同一个共享变量时特别容易出现并发问题,特别是在多个线程需要对一个共享变量进行写入时。为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步,如图1-3所示。        同步的措施一般是加锁,这就需要使用者对锁有......
  • Java技术深度探索:高并发场景下的线程安全与性能优化
    Java技术深度探索:高并发场景下的线程安全与性能优化在当今的软件开发领域,随着互联网应用的日益复杂和用户量的激增,高并发成为了一个不可忽视的技术挑战。Java,作为一门广泛应用于企业级开发的编程语言,其内置的并发支持机制如线程(Thread)、锁(Lock)、并发集合(ConcurrentCollect......
  • Python的进程池和线程池
    线程池都在一个进程内GIL不适合密集型计算共享进程资源,存在不安全的线程fromconcurrent.futuresimportThreadPoolExecutordeftask(n):returnn*2if__name__=='__main__':withThreadPoolExecutor(max_workers=4)asexecutor:results=......
  • 说下Python中的各个多线程模块之间的区别
    在Python中,涉及多线程的主要模块有threading、thread(在Python2.x中使用)和concurrent.futures。以下是这些模块之间的详细区别:1.threading模块简介:threading是Python的标准库之一,提供了创建和管理线程的高级接口。特点:线程类:提供Thread类,用户可以通......
  • Python中的 GIL是什么?它如何影响多线程?
    GIL(GlobalInterpreterLock)GIL(全局解释器锁)是Python解释器(特别是CPython实现)中的一个机制,用于管理对Python对象的访问。由于Python的内存管理不是线程安全的,GIL确保在任意时刻只有一个线程可以执行Python字节码,从而避免了多个线程同时访问和修改对象造成的数据不一致......