深入理解并发List、Set、ConcurrentHashMap底层原理

之前两篇分析了并发的三大特性和JMM模型,从硬件、jvm、java层面分别进行分析。JMM模型是并发当中最难理解的部分,涉及到的概念很多。
本次分析经常使用到的集合的底层原理和数据结构。

深入ArrayList分析

ArrayList使我们经常使用到的List的实现类。先来看先它有什么特点。
首先List的特点:元素有放入顺序,元素可重复 。ArrayList自然也有相同的特点。
其次ArrayList的存储结构底层采用数组来实现的。什么是数组:数组是采用一段连续的存储单元来存储数据。什么是连续的存储单元,在jvm中对象会在堆中分配空间,那么数组呢同样在堆中分配空间并且是一段连续的空间。
数组解析
数组是一个数据结构,它的特点是查询的时间复杂度O(1),插入的时间复杂度O(N)。就是说查询速度快,插入速度慢。

上图定义了一个数组。这个数组每一个元素都要以一个下标,下标从0开始,0指向了数组起始位置在内存中的地址,也就是数组的起始内存地址。从上图来说0指向了起始地址为100,这个100是内存中的地址。
如果要查找数组的元素怎么查找呢,比如要查找a[2]的元素。这里有一个数组的查找公式:a[n]=起始地址+(n*字节数)。int类型的字节数是4,那么a[2]的内存地址=100+(2*4)=108。那么找到内存地址是108的位置获取到对应的值就是34。这就是查询的时间复杂度O(1)的概念。这里的1不是操作一次的意思,而是一个常量的意思。数组是本身就是一个连续的空间,所以知道起始位置去找其他位置上的元素是非常快的。这就是数组查询速度快的原因。
在来看下数组插入的过程

如上图,向数组插入数据d,比如插入到2和3之间,首先3会移动一定空间让d插入,由于下标是连续的,当d插入以后,d后面数据的下标就要往前挪一个位置,本来下标3指向e,现在下标3指向d,一次类推。所以数组插入的时间复杂度是O(N)。当然了如果向数组的最后一个下标插入元素那么时间复杂度就是O(1),但是从数组的角度来说这样的概率1/n,概率较小,从常规来说数组插入的时间复杂度是O(N)。在理解了数组后来看下ArrayList底层数组是如何实现的。
ArrayList源码分析
怎么证实ArrayList底层是数组实现的呢?先来看add方法
add方法


通过上面这两张图就能很好看出,ArrayList底层是通过Object数组来实现。
那么再来看看这个add方法的时间复杂度是多少?这个add的方法时间复杂度为O(1)。这里是默认从数组的最后一位插入。虽然说数组的插入比较慢,但是在ArrayList的这个add方法中插入效率是非常高的。

在执行elementData[size++] = e赋值之前呢先执行了ensureCapacityInternal(size + 1); 看下这部分源码

这部分代码主要是用来扩容的。具体在 grow(minCapacity);

扩容机制的底层实现通过数组拷贝(浅拷贝)的方式。这个数组拷贝是说原来数组长度为6,扩容的时候重新开辟一个新的数组长度为12,然后把原来那个长度为6的数组拷贝到新的数组当中去。这样做的好处是什么呢,利用空间获取时间。如果说直接在原数组上扩容六个位置,那么可能导致下标要向前或者向后挪动,就像数组插入流程那样,插入六个位置。这样做的缺点就是占用了两份数组的空间,所以说是利用空间换取时间
像使用redis做缓存,减少对数据库的压力也是一种空间换时间的做法。
再来看下另一个add方法

那么这个add方法的时间复杂度是O(N),它是指定下标插入。
ArrayList还有常用的get()方法,这个方法实际上就是用了数组查询的公式,a[n]=起始地址+(n*字节数)。比较简单,上面分析过了就不在分析了。
再来看下ArrayList类的关系

RandomAccess是一个随机访问的接口。
Cloneable是一个拷贝的接口,解释一下:实现Cloneable接口,重写clone方法、方法内容默认调用父类的clone方法。像浅拷贝、深拷贝要实现这个接口。
Serializable是一个序列化接口
继承了AbstractList ,说明它是一个列表,拥有相应的增,删,查,改等功能。
为什么继承了 AbstractList 还需要实现List 接口?据说是作者写错了感兴趣可以去了解下

ArrayList中定义两个空数组有什么作用



调用有参构造和无参构造分别使用到了这两个空数组。有什么用呢,就是说如果在new ArrayList的时候调用有参构造指定了大小,那么add方法中就不会去判断扩容,而是直接使用指定的大小,相当于提高性能。
总结一下:ArrayList底层使用数组来实现的,满足数组的特性查询快插入慢不指定下标的add方法默认是从最后一个下标开始插入数据,它的插入效率也挺高。
ArrayList实现了List满足List的特点:有序的可重复的

深入LinkedList分析

LinkedList的继承关系和ArrayList一样。但是底层是通过链表实现的。
LinkedList的特点:有序的可重复的,由于底层使用链表实现,查询效率低,插入效率高
链表分析
链表的定义:链表是一种在物理单元存储上非连续、非顺序的数据结构。
链表特点:插入删除的时间复杂度O(1),查询的时间复杂度O(N),就是插入块查询慢
看下链表在java当中的代码实现

public class NodeDemo {//存储的数据private Object data;//指向下一个节点public NodeDemo next;public NodeDemo(Object data){this.data = data;}public static void main(String[] args) {NodeDemo head = new NodeDemo("刘备");//相当于链表增加操作head.next = new NodeDemo("关羽");//head.next = null //相当于链表删除操作head.next.next = new NodeDemo("张飞");System.out.println(head.data);System.out.println(head.next.data);System.out.println(head.next.next.data);}
}


对于链表查询需要从头节点遍历整个链表,所以比较慢,链表插入删除对next指针的修改所以比较快。
LinkedList源码分析
LinkedList的类继承关系和ArrayList一样。
add方法:



通过上面的源码,可以看到LinkedList的层一个双向链表。链表分为单向链表、双向链表、循环链表。LinkedList插入删除方法和上图链表的插入删除过程相同。

插入相同数据量的话LinkedList和ArrayList那个会更快。如果说ArrayList没有指定容量的话,因为涉及到扩容问题LinkedList的插入会更快。如果ArrayList指定了容量的话,不存在扩容问题,那么会ArrayList的会更快,因为ArrayList的add方法是默认往数组最后一个元素插入。

HashSet分析

特点: 元素无放入顺序,元素不可重复(注意:元素虽然无放入顺序,但是元素在set中的位置是有该元素的HashCode决定的,其位置其实是固定的)
底层采用HashMap来实现
add方法

底层通过map进行数据添加,所以元素不可重复。对于HashSet分析重点在于对HashMap的分析

深入HashMap分析

HashMap的底层结构采用数组、链表、红黑树来实现。
程序=数据结构+算法。现在知道HashMap的数据结构,那么来看看HashMap的算法。
哈希算法(也叫散列),就是把任意长度值(Key)通过散列算法变换成固定长度的key(地址) 通过这个地址进行访问的数据结构它通过把关键码值映射到表中一个。位置来访问记录,以加快查找的速度。
这是哈希算法的原理,有很多种实现方式,例如MD5。那我们怎么实现一个HashMap的功能。


上图就是把一个名字转成对应的ascll,进行取模,这样就能放到对应下标,比如对429取模10得到9,那么就放入到9的下标当中。那么问题来了,为什么要取模?为了节省空间,数组的定义是一个连续的存储单元,如果ascll成千上万,那要开辟成千上万的空间只为存储一个名字,明显不合理,所以要进行取模处理。
那么还要一个问题,看下图

得到相同ascll通过散列算法之后会放入数组对应下标的当中,导致lies放入到9的位置,foes也放入到9的位置,那么就会覆盖了之前值。为了避免这种情况出现,引入了链表结构。这就是为什么HashMap既有数组又有链表的原因。

那么为什么HashMap还要用到红黑树呢?主要是解决链表查询慢的问题,在上面说过链表查询时找到头节点,根据next节点找到下一个元素,说白了就是要遍历整个链表。如果链表太长那么查询效率就会非常低下。

刚才说了HashMap的计算出hashCode相同的时候,会把相同数据插入到链表中,那么具体怎么插入。在JDK1.7的时候,采用的是头插法。就像上图,如果先插入foes,那么HashMap会把foes放入到数据中,因为此时只有一个429的hashCode。如果又来一个lies,它的hashCode也是429,那么foes会让出位置,让lies插入到数组中,然后lies的指针指向foes。这就是头插法。
在JDK1.8的时候采用的是尾插法。插入的方向相反。foes的指针指向lies。
在JDK1.7的时候这种头插法在并发的情况下,如果使用不当会出现cpu100%的问题。这里就不扩展了。在JDK1.8采用了尾插法,解决了这个问题。
HashMap源码分析
先来看put方法

重点来看putVal方法

第一个if判断map是否为空,然后初始化的操作。第二个if就是判断tab存储hashcode有没有重复,如果没有采用数组方式存储。如果有采用链表方式存储。再来看看else的部分代码 。

else的部分就是采用链表和红黑树的存储方式,红框表示的是采用红黑树的条件,调用treeifyBin方法就是将链表转成红黑树。treeifyBin的源码就不分析了。

HashMap定义两个常量,上面一个是常量链表转成红黑树的条件,下面一个常量等于6是红黑树转成链表的条件。就是说如果对红黑树的数据删除,长度小于6那么就会转成链表。
HashMap定义很多常量例如扩容的常量和加载因子的常量,就不在一一解释了。说一下加载因子HashMap默认数组长度是16,加载因子定义为0.75,就是说当插入的数据达到12个是开始扩容。
下图是HashMap的put方法的流程图

深入ConcurrentHashMap分析

存储结构: 底层采用数组、链表、红黑树 内部大量采用CAS操作。并发控制使⽤synchronized 和 CAS 来操作来实现的。
并发安全的HashMap ,比Hashtable效率更高。
Hashtable同样也是线程安全的,看下它的put方法

Hashtable的put方法是方法级别的synchronized,相对于ConcurrentHashMap来说,锁的范围更大,细粒度更大。不过synchronized一直在优化,性能并没有想象中那么差,用还是能够使用的。

下面来分析ConcurrentHashMap的源码。ConcurrentHashMap在jdk1.7和1.8差别比较大,在1.8做了很多优化,JDK1.8ConcurrentHashMap的源码流程和HashMap的源码流程差不多。
ConcurrentHashMap的源码分析
还是以put方法为例子

和HashMap一样调用了putVal方法。

final V putVal(K key, V value, boolean onlyIfAbsent) {//空置判断if (key == null || value == null) throw new NullPointerException();//两次hashint hash = spread(key.hashCode());int binCount = 0;//采用了自旋方式对table进行遍历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) {//用数组方式存储,这里用到了CAS的方式保证线程安全性if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))break;                   // no lock when adding to empty bin}//判断状态如果是MOVED的状态进行扩容else if ((fh = f.hash) == MOVED)//扩容的方法, 底层也是采用了CAS方式进行扩容tab = helpTransfer(tab, f);else {//采用链表或者红黑树存储V oldVal = null;//对链表或者红黑树上锁,这里锁的粒度很小//后面代码判断转成链表还是转成红黑树,逻辑和HashMap差不多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) {if (binCount >= TREEIFY_THRESHOLD)treeifyBin(tab, i);if (oldVal != null)return oldVal;break;}}}addCount(1L, binCount);return null;}

上面代码的流程及时先做一些判断,然后遍历table。遍历的这个table是定义的一个volatile类型的


遍历的过程中定义一些变量,然后判断要不要初始化(执行 tab = initTable();)。为什么要在put方法里面进行初始化,首先ConcurrentHashMap的构造方法中并没有做任何初始化处理。除非你指定了容量。

再来看initTable()方法

这段代码意思是判断有没有初始化,如果初始化了那个当前线程就要让出时间片,就是不用执行初始化了。如果没有初始化,则采用CAS方式,然后进行扩容。
后续代码执行流程,在上面源码上注释有说明了。
ConcurrentHashMap和HashMap的处理逻辑差不多。差别在于,ConcurrentHashMap在put方法中判断需不需要执行初始化(采用CAS方式保证安全性),然后通过自旋的方式对整个table进行操作。判断是采用数组方式存储(数组方式存储时使用的是CAS方式保证安全性),还是链表或者红黑树(使用这两种方式存储时采用的是synchronized 上锁,保证安全性,这样锁的粒度很小)。

那么采用自旋的方式和采用synchronized 的 方式有什么差别?首先这是阻塞和不阻塞的区别,还有一个是自旋和CAS是不需要改变线程的状态,synchronized 就会改变线程的状态。线程状态的改变会影响性能。

并发编程三:深入理解并发List、Set、ConcurrentHashMap底层原理相关推荐

  1. 并发操作之——并发编程三要素

    并发操作 并发操作之--并发编程三要素. 并发操作之--并发编程三要素 并发操作 前言 一.原子性 二.有序性: 三.可见性: 总结 前言 并发操作之--并发编程三要素. 一.原子性 一个不可再被分割 ...

  2. 并发编程之深入理解java线程

    并发编程之深入理解java线程 一.线程基础知识 1.1 进程和线程 1.1.1 进程 1.1.2 线程 1.1.3 进程与线程的区别 1.1.4 进程间通信的方式 1.2 线程的同步互斥 1.3 上 ...

  3. 并发编程之深入理解synchronized

    并发编程之深入理解synchronized 一.java共享内存带来的线程安全问题 1.1 问题分析 1.2 临界区 1.3 竞态条件 二.synchronized使用 2.1 解决之前的共享问题 三 ...

  4. 并发编程之深入理解JMM并发三大特性volatile

    并发编程之深入理解JMM&并发三大特性&volatile 并发和并行 并发三大特性 可见性 有序性 原子性 Java内存模型(JMM) JMM定义 JMM与硬件内存架构的关系 内存交互 ...

  5. php三要素,并发编程三要素:原子性,有序性,可见性

    并发编程三要素 **原子性:**一个不可再被分割的颗粒.原子性指的是一个或多个操作要么全部执行成功要么全部执行失败. 有序性: 程序执行的顺序按照代码的先后顺序执行.(处理器可能会对指令进行重排序) ...

  6. 【并发编程三】C++进程通信——管道(pipe)

    [并发编程三]C++实现通信--管道(pipe) 一.管道(pipe) 二.匿名管道 1.简介 2.父子进程:匿名管道的通信过程? 3.相关函数 3.1.创建管道CreatePipe 3.2.写入管道 ...

  7. 并发编程-初级之认识并发编程

    并发编程-初级之认识并发编程 1.并发领域可以处理的问题 分工: 同步:分好工之后,就可以具体执行.任务之间是有依赖的,一个任务结束之后将去去通知后续的任务.java里面 Executor.Fork/ ...

  8. 徐无忌并发编程笔记:无锁机制CAS及其底层实现原理?

    徐无忌并发编程笔记:无锁机制CAS及其底层实现原理? 完成:第一遍 1.什么是CAS算法? compare and swap:比较与交换,是一种有名的无锁算法 CAS带来了一种无锁解决线程同步,冲突问 ...

  9. 《Java并发编程入门与高并发面试》or 《Java并发编程与高并发解决方案》笔记

    <Java并发编程入门与高并发面试>or <Java并发编程与高并发解决方案>笔记 参考文章: (1)<Java并发编程入门与高并发面试>or <Java并发 ...

最新文章

  1. commander.js
  2. 19个超赞的数据科学和机器学习工具,编程小白必看!(附资料)
  3. video thumbnails
  4. uni-app微信小程序登录;uni-app微信登录小程序;uni-app微信登录app;
  5. HTML5 Canvas 图形组合
  6. Java教程:使用记事本编写运行Java程序
  7. ZK在Eclipse下的环境搭建
  8. poj1083 解题报告(poj 1083 analysis report)
  9. redis_lua_nginx环境配置过程
  10. M1卡读写软件C#源代码
  11. AD9361开发:接收与发送滤波器配置
  12. keil中断函数的写法_中断函数写法的比较
  13. ubuntu18与win10双系统引导修复
  14. 【c项目】网吧管理系统的设计和实现
  15. HIT 软件构造 lab3实验报告
  16. Microsoft Edge浏览器插件(1)
  17. Github 未添加密钥报错
  18. 西方哲学史 -- 赫拉克利特
  19. vue整合videojs插件,播放RTMP,hls直播视频
  20. Alibaba Java Coding Guidelines安装使用教程

热门文章

  1. MOTO X的截屏快捷键 -》只要按住 电源键+音量减小健 就可以了
  2. 【去噪】A Physics-Based Noise Formation Model for Extreme Low-Light Raw Denoising噪声建模详解
  3. 【读书笔记】高效能人士的7个习惯
  4. TransportClient使用总结
  5. 【智能金融】BCG报告:智慧运营,银行业竞争的下一个决胜之地
  6. 码住这几个方法,轻松实现图片批量旋转
  7. 支付宝公布的扫码点餐激励2.0,一个门店奖励200
  8. Java、JSP公交管理系统的设计
  9. 计算机如何通过手机连接网络打印机,如何让手机连接上打印机,原来是这样的...
  10. 解决nginx+django+uwsgi出现编码问题UnicodeEncodeError: ‘ascii‘ codec can‘t encode characters in position 0-1: