concurrenthashmap_ConcurrentHashMap是如何保证线程安全的

文章已同步发表于微信公众号JasonGaoH,ConcurrentHashMap是如何保证线程安全的

之前分析过HashMap的一些实现细节,关于HashMap你需要知道的一些细节, 今天我们从源码角度来看看ConcurrentHashMap是如何实现线程安全的,其实网上这类文章分析特别多,秉着”纸上得来终觉浅,绝知此事要躬行“的原则,我们尝试自己去分析下,希望这样对于ConcurrentHashMap有一个更深刻的理解。

为什么说HashMap线程不安全,而ConcurrentHashMap就线程安全

其实ConcurrentHashMap在Android开发中使用的场景并不多,但是ConcurrentHashMap为了支持多线程并发这些优秀的设计却是最值得我们学习的地方,往往”ConcurrentHashMap是如何实现线程安全“这类问题却是面试官比较喜欢问的问题。

首先,我们尝试用代码模拟下HashMap在多线程场景下会不安全,如果把这个场景替换成ConcurrentHashMap会不会有问题。

因为不同于其他的线程同步问题,想模拟出一种场景来表明HashMap是线程不安全的稍微有点麻烦,可能是hash散列有关,在数据量较小的情况下,计算出来的hashCode是不太容易产生碰撞的,网上很多文章都是尝试从源码角度来分析HashMap可能会导致的线程安全问题。

我们来看下下面这段代码,我们构造10个线程,每个线程分别往map中put 1000个数据,为了保证每个数据的key不一样,我们将i+ 线程名字来作为map 的key,这样,如果所有的线程都累加完的话,我们预期的map的size应该是10 * 1000 = 10000。

import java.util.HashMap;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;public class HashMapTest { public static void main(String[] args) {  Map map = new HashMap();  //     Map map = new ConcurrentHashMap();  for (int i = 0; i  1)   Thread.yield();    System.out.println(map.size()); }}class MyThread extends Thread {    public Map map;    public String name;    public MyThread(Map map, String name) {      this.map = map;      this.name = name;    }    public void run() {     for(int i =0;i<1000;i++) {      map.put(i + name, i + name);     }    }  }

使用HashMap,程序运行,结果如下:

9930

那我们如果把这里的HashMap换成ConcurrentHashMap来试试看看效果如何,输出结果如下:

10000

我们发现不管运行几次,HashMap的size都是小于10000的,而ConcurrentHashMap的size都是10000。从这个角度也证明了ConcurrentHashMap是线程安全的,而HashMap则是线程不安全的。 HashMap在多线程put的时候,当产生hash碰撞的时候,会导致丢失数据,因为要put的两个值hash相同,如果这个对于hash桶的位置个数小于8,那么应该是以链表的形式存储,由于没有做通过,后面put的元素可能会直接覆盖之前那个线程put的数据,这样就导致了数据丢失。

其实列举上面这个例子只是为了从一个角度来展示下为什么说HashMap线程不安全,而ConcurrentHashMap则是线程安全的,鉴于HashMap线程安全例子比较难列举出来,所以才通过打印size这个角度来模拟了下。

这篇文章深入解读HashMap线程安全性问题就详细介绍了HashMap可能会出现线程安全问题。 文章主要讲了两个可能会出现线程不安全地方,一个是多线程的put可能导致元素的丢失,另一个是put和get并发时,可能导致get为null,但是也仅是在源码层面分析了下,因为这种场景想要完全用代码展示出来是稍微有点麻烦的。

接下来我们来看看ConcurrentHashMap是如何做到线程安全的。

JDK8的ConcurrentHashMap文档提炼

  • ConcurrentHashMap支持检索的完全并发和更新的高预期并发性,这里的说法很有意思检索支持完全并发,更新则支持高预期并发性,因为它的检索操作是没有加锁的,实际上检索也没有必要加锁。
  • 实际上ConcurrentHashMap和Hashtable在不考虑实现细节来说,这两者完全是可以互相操作的,Hashtable在get,put,remove等这些方法中全部加入了synchronized,这样的问题是能够实现线程安全,但是缺点是性能太差,几乎所有的操作都加锁的,但是ConcurrentHashMap的检测操作却是没有加锁的。
  • ConcurrentHashMap检索操作(包括get)通常不会阻塞,因此可能与更新操作(包括put和remove)重叠。
  • ConcurrentHashMap跟Hashtable类似但不同于HashMap,它不可以存放空值,key和value都不可以为null。

印象中一直以为ConcurrentHashMap是基于Segment分段锁来实现的,之前没仔细看过源码,一直有这么个错误的认识。ConcurrentHashMap是基于Segment分段锁来实现的,这句话也不能说不对,加个前提条件就是正确的了,ConcurrentHashMap从JDK1.5开始随java.util.concurrent包一起引入JDK中,在JDK8以前,ConcurrentHashMap都是基于Segment分段锁来实现的,在JDK8以后,就换成synchronized和CAS这套实现机制了。

JDK1.8中的ConcurrentHashMap中仍然存在Segment这个类,而这个类的声明则是为了兼容之前的版本序列化而存在的。

   /**     * Stripped-down version of helper class used in previous version,     * declared for the sake of serialization compatibility.     */    static class Segment extends ReentrantLock implements Serializable {        private static final long serialVersionUID = 2249069246763182397L;        final float loadFactor;        Segment(float lf) { this.loadFactor = lf; }    }

JDK1.8中的ConcurrentHashMap不再使用Segment分段锁,而是以table数组的头结点作为synchronized的锁。和JDK1.8中的HashMap类似,对于hashCode相同的时候,在Node节点的数量少于8个时,这时的Node存储结构是链表形式,时间复杂度为O(N),当Node节点的个数超过8个时,则会转换为红黑树,此时访问的时间复杂度为O(long(N))。

 /**     * The array of bins. Lazily initialized upon first insertion.     * Size is always a power of two. Accessed directly by iterators.     */    transient volatile Node[] table;

数据结构图如下所示:

64a2f60662d1516b7275af4d56c5eb02.png

其实ConcurrentHashMap保证线程安全主要有三个地方。

一、使用volatile保证当Node中的值变化时对于其他线程是可见的

二、使用table数组的头结点作为synchronized的锁来保证写操作的安全

三、当头结点为null时,使用CAS操作来保证数据能正确的写入。

使用volatile

可以看到,Node中的val和next都被volatile关键字修饰。

volatile的happens-before规则:对一个volatile变量的写一定可见(happens-before)于随后对它的读。

也就是说,我们改动val的值或者next的值对于其他线程是可见的,因为volatile关键字,会在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据。

static class Node implements Map.Entry {        final int hash;        final K key;        volatile V val;        volatile Node next;  }  ...

另外,ConcurrentHashMap提供类似tabAt来读取Table数组中的元素,这里是以volatile读的方式读取table数组中的元素,主要通过Unsafe这个类来实现的,保证其他线程改变了这个数组中的值的情况下,在当前线程get的时候能拿到。

 static final  Node tabAt(Node[] tab, int i) {        return (Node)U.getObjectVolatile(tab, ((long)i <

而与之对应的,是setTabAt,这里是以volatile写的方式往数组写入元素,这样能保证修改后能对其他线程可见。

 static final  void setTabAt(Node[] tab, int i, Node v) {        U.putObjectVolatile(tab, ((long)i <

我们来看下ConcurrentHashMap的putVal方法:

  /** 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[] tab = table;;) {            Node f; int n, i, fh;            if (tab == null || (n = tab.length) == 0)                tab = initTable();            //当头结点为null,则通过casTabAt方式写入            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {                if (casTabAt(tab, i, null,                             new Node(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;                //头结点不为null,使用synchronized加锁                synchronized (f) {                    if (tabAt(tab, i) == f) {                        if (fh >= 0) {                            //此时hash桶是链表结构                            binCount = 1;                            for (Node 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 pred = e;                                if ((e = e.next) == null) {                                    pred.next = new Node(hash, key,                                                              value, null);                                    break;                                }                            }                        }                        else if (f instanceof TreeBin) {                            //此时是红黑树                            Node p;                            binCount = 2;                            if ((p = ((TreeBin)f).putTreeVal(hash, key,                                                           value)) != null) {                                oldVal = p.val;                                if (!onlyIfAbsent)                                    p.val = value;                            }                        }                        else if (f instanceof ReservationNode)                            throw new IllegalStateException("Recursive update");                    }                }                if (binCount != 0) {                    //当链表结构大于等于8,则将链表转换为红黑树                    if (binCount >= TREEIFY_THRESHOLD)                        treeifyBin(tab, i);                    if (oldVal != null)                  return oldVal;                    break;                }            }        }        addCount(1L, binCount);        return null;    }

在putVal方法重要的地方都加了注释,可以帮助理解,现在我们一步一步来看putVal方法。

使用CAS

当有一个新的值需要put到ConcurrentHashMap中时,首先会遍历ConcurrentHashMap的table数组,然后根据key的hashCode来定位到需要将这个value放到数组的哪个位置。

tabAt(tab, i = (n - 1) & hash))就是定位到这个数组的位置,如果当前这个位置的Node为null,则通过CAS方式的方法写入。所谓的CAS,即即compareAndSwap,执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。

这里就是调用casTabAt方法来实现的。

     static final  boolean casTabAt(Node[] tab, int i,                                        Node c, Node v) {        return U.compareAndSwapObject(tab, ((long)i <

casTabAt同样是通过调用Unsafe类来实现的,调用Unsafe的compareAndSwapObject来实现,其实如果仔细去追踪这条线路,会发现其实最终调用的是cmpxchg这个CPU指令来实现的,这是一个CPU的原子指令,能保证数据的一致性问题。

使用synchronized

当头结点不为null时,则使用该头结点加锁,这样就能多线程去put hashCode相同的时候不会出现数据丢失的问题。synchronized是互斥锁,有且只有一个线程能够拿到这个锁,从而保证了put操作是线程安全的。

下面是ConcurrentHashMap的put操作的示意图,图片来自于ConcurrentHashMap源码分析(JDK8)get/put/remove方法分析

d445e40f2106170d7e9586d6c7729e1e.png

参考文章

从ConcurrentHashMap的演进看Java多线程核心技术

ConcurrentHashMap源码分析(JDK8)get/put/remove方法分析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/336163.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

计算机系统计算机,计算机系统与计算机化系统的区别

2010版GMP正文中提到“计算机”的地方一共三处&#xff0c;两处用的是计算机化&#xff0c;一处用的是“计算机”。在GMP的术语条款&#xff0c;则只提到了“计算机化系统”&#xff0c;2015年12月生效的GMP附录中的表述也是“计算机化系统”。那么“计算机系统”与“计算机化系…

【OpenGL从入门到精通(三)】第一个点的理论

OpenGL状态机 一&#xff0c;OpenGL是一个状态机matrix中包括&#xff1a; model view (模型矩阵) worldpos(世界坐标,也称为顶点坐标)通过mv(模型矩阵)转到cameru摄像机下&#xff08;根据右手坐标系&#xff0c;只有在Z的负方向才可以看得见&#xff09; projection …

cloud foundry_将Spring Boot应用程序绑定到Cloud Foundry中的服务的方法

cloud foundry如果要试用Cloud Foundry &#xff0c;最简单的方法是下载出色的PCF开发人员或在Pivotal Web Services站点上创建试用帐户。 其余文章假定您已经安装了Cloud Foundry&#xff0c;并且对Cloud Foundry有了较高的了解。 这篇文章的目的是列出将Java应用程序集成到服…

js find的用法_React常用库Immutable.js常用API

JavaScript 中的对象一般是可变的(Mutable)&#xff0c;因为使用了引用赋值&#xff0c;新的对象简单的引用了原始对象&#xff0c;改变新的对象将影响到原始对象。如 foo{a: 1}; barfoo; bar.a2 你会发现此时 foo.a 也被改成了 2。虽然这样做可以节约内存&#xff0c;但当应用…

教学案例 计算机,计算机教学案例

海量优秀的免费计算机教学案例范文供您参考与下载&#xff0c;关于计算机教学案例的免费论文范文参考资料是由2016年最新的相关论文题目按照标准论文格式模板写作的,适合不知道怎么写计算机教学案例的大学毕业生,对相关的本科论文和硕士毕业论文及职称论文发表写作有参考价值&a…

java 开发:md5_Java社区调查结果:74%的开发人员希望减少详细程度

java 开发:md5一个新的JDK增强建议&#xff08;JEP&#xff09;在Java社区中风起云涌&#xff1a;JEP286。该建议建议在Java的未来版本中引入局部变量类型推断&#xff0c;以简化Java应用程序的编写。 在下面的文章中&#xff0c;我们将解释它的含义以及它将如何影响您的代码。…

python获取当前目录_又有几个Python小技巧分享

今天这篇文章为大家带来几个Python使用的小技巧&#xff1a;快速生成依赖文件文件路径处理通过set将对象去重单元测试unitest捕获异常快速生成依赖文件Python通过requirements.txt来管理项目所依赖的库&#xff0c;通过pip install -r requirements.txt命令可以直接安装项目所需…

计算机弹歌曲做我老婆好不好,做我老婆好不好吉他谱(图片谱,指弹,做我老婆好不好,徐誉滕)_徐誉滕(徐海;徐歆舜)...

做我老婆好不好词曲&#xff1a;徐誉滕演唱&#xff1a;徐誉滕走过多少路口 听过多少叹息我认真着你的不知所措这种迷茫心情 我想谁都会有幸运的是能分担你的愁能不能靠近一点 能不能再近一点满足我心中小小的虚荣其实你并不知道 在我心中你最美就像风雨过后天边的那道彩虹如果…

python敏感字替换_python用类实现文章敏感词的过滤方法示例

过滤一遍并将敏感词替换之后剩余字符串中新组成了敏感词语,这种情况就要用递归来解决&#xff0c;直到过滤替换之后的结果和过滤之前一样时才算结束 第一步:建立一个敏感词库(.txt文本)第二步:编写代码在文章中过滤敏感词(递归实现) # -*- coding: utf-8 -*- # author 代序春秋…

2018年冷链百强_在分析了47,251个依赖关系之后,2016年Java图书馆百强

2018年冷链百强谁在上面&#xff0c;谁在后面&#xff1f; 我们分析了Github上的47,251个依赖关系&#xff0c;并抽取了前100个Java库 对于长周末而言&#xff0c;我们最喜欢的消遣是浏览Github并搜索流行的Java库。 我们决定与您分享乐趣和信息。 我们分析了Github上前3,862…

【FFMPEG应用篇】基于FFmpeg的封装格式转换

/* * 一笑奈何 * cn-yixiaonaihe.blog.csdn.net */#include <iostream> #include <thread> extern "C" { #include "libavformat/avformat.h" #include "libavcodec/avcodec.h" #include "libswscale/swscale.h" #inc…

计算机键盘标注,电脑键盘上怎么打√和×

用电脑键盘上打出√和两个符号的方法1、在输入法开启状态下&#xff0c;鼠标右键点击输入法状态栏上面的“软键盘图标”&#xff0c;然后选择“数学符号”(直接右击打不开就先左击再右击)。2、在数学符号的软键盘面板上直接用鼠标点击“√和”就好(光标要定位在需要输入的地方)…

python中运算符号怎样表示_Python中的运算符与表达式

你所编写的大多数语句&#xff08;逻辑行&#xff09;都包含了表达式&#xff08;Expressions&#xff09;。一个表达式的简单例子便是 23。表达式可以拆分成运算符&#xff08;Operators&#xff09;与操作数&#xff08;Operands&#xff09;。 运算符&#xff08;Operators&…

web服务优化与健壮性改进_创建健壮的微服务架构所涉及的组件

web服务优化与健壮性改进在本文中&#xff0c;我们将简要学习构建强大的微服务应用程序所需的各种软件组件。 在简要了解每个架构组件之前&#xff0c;我们将陈述设计微服务架构时出现的一般查询。 1.微服务架构组件 每当我们创建微服务应用程序时&#xff0c;我们都会想到以下…

计算机对油画的影响,现代电脑美术影响下的油画艺术创作

摘要&#xff1a;"21世纪的文盲不是不识字,而是不会读图."随着视觉文化和电脑网络的快速发展,计算机技术也广泛地涵盖了整个意识形态领域.图像已成为这个时代的最大资源,我们已经快速进入"读图时代".九十年代起,计算机图形艺术在科技的带动下飞速发展,电脑…

【FFMPEG应用篇】基于FFmpeg的RGB格式封装MOV文件

/* * 一笑奈何 * cn-yixiaonaihe.blog.csdn.net */#include <iostream> #include <thread> extern "C" { #include "libavformat/avformat.h" #include "libavcodec/avcodec.h" #include "libswscale/swscale.h" #inc…

python launcher卸载后蓝屏_误卸载python2.4导致yum不能用后的修复

去 http://mirrors.ustc.edu.cn/centos/或者镜像下载如下包&#xff0c;版本不一定非常一致 python-2.4.3-56.el5.x86_64.rpm python-devel-2.4.3-56.el5.i386.rpm python-devel-2.4.3-56.el5.x86_64.rpm python-iniparse-0.2.3-6.el5.noarch.rpm python-libs-2.4.3-56.el5.x86…

owin 怎么部署在云中_使用Boxfuse轻松在云中运行Spring Boot应用程序

owin 怎么部署在云中几天前&#xff0c;我开始构建一个将使用REST API检索和存储数据的iOS应用。 该REST API将是我也必须构建的服务器应用程序。 由于我熟悉Java和Spring &#xff0c;因此决定使用Spring Boot作为框架。 为了能够在我的iPhone上使用它&#xff0c;如果我可以在…

【FFMPEG应用篇】基于FFmpeg的PCM数据编码为AAC

/* * 一笑奈何 * cn-yixiaonaihe.blog.csdn.net */#include <iostream> #include <thread> extern "C" { #include "libavformat/avformat.h" #include "libavcodec/avcodec.h" #include "libswscale/swscale.h" #inc…

计算机过程控制系统实例ppt,chap7 前馈控制系统_武汉理工大学:调节仪表与过程控制系统_ppt_大学课件预览_高等教育资讯网...

第七章 前馈及复合控制系统其他反馈控制的缺点&#xff1a;无法将干扰克服在被控制量偏离设计值之前。被控对象总是存在一定的纯滞后和容量滞后&#xff0c;故限制了控制作用的充分发挥。 7-1 基本概念1、问题的提出2、技术思路直接按扰动而不是按偏差进行控制。干扰发生后&…