1. ConcurrentHashMap

2. ConcurrentLinkedQueue

3. ConcurrentSkipListMap

4. ConcurrentSkipListSet

5. txt

 java 并发容器
ConcurrentHashMap
并发编程中需要用到线程安全的HashMap
为什么要用
1. 线程不安全的HashMap
数据丢失:put的时候导致的多线程数据不一致
比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的 hash桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的 hash桶索引和线程B要插入的记录计算出来的 hash桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。
死循环:resize而引起死循环(JDK1.8已经不会出现该问题)
这种情况发生在JDK1.7 中HashMap自动扩容时,当2个线程同时检测到元素个数超过 数组大小 × 负载因子。此时2个线程会在put()方法中调用了resize(),两个线程同时修改一个链表结构会产生一个循环链表(JDK1.7中,会出现resize前后元素顺序倒置的情况)。接下来再想通过get()获取某一个元素,就会出现死循环,导致CPU利用率接近100%。
jdk1.8版本中多线程put不会在出现死循环问题了,只有可能出现数据丢失的情况,因为1.8版本中,会将原来的链表结构保存在节点e中,然后依次遍历e,根据hash&n是否等于0,分成两条支链,保存在新数组中。jdk1.7版本中,扩容过程中会新数组会和原来的数组有指针引用关系,所以将引起死循环问题。
2. 效率低下的HashTable
HashTable使用Sychronized来保证线程安全,在线程激烈的情况下,效率低下
当一个线程访问 HashTable 的同步方法时,其他线程如果也要访问同步方法,会被阻塞住。因此Hashtable效率很低,基本被废弃。
3. ConcurrentHashMap的锁分段技术可有效提升并发访问率
jdk1.7 数组+链表
jdk1.8 数组+链表/红黑树
jdk1.8
volatile + CAS + Synchronized 来保证并发更新的安全,底层采用数组+链表/红黑树的存储结构
示意图
重要内部类
Node
key-value键值对
实现了Map.Entry接口,用于存储数据。它对value和next属性设置了volatile同步锁,只允许对数据进行查找,不允许进行修改
TreeNode
红黑树节点
继承于Node,但是数据结构换成了二叉树结构,它是红黑树的数据的存储结构,用于红黑树中存储数据,当链表的节点数大于8时会转换成红黑树的结构,他就是通过TreeNode作为存储结构代替Node来转换成黑红树。
TreeBin
就相当于一颗红黑树,其构造方法其实就是构造红黑树的过程
TreeBin是封装TreeNode的容器,它提供转换黑红树的一些条件和锁的控制。
ForwardingNode
辅助节点,用于ConcurrentHashMap扩容操作
一个用于连接两个table的节点类。它包含一个nextTable指针,用于指向下一张表。而且这个节点的key value next指针全部为null,它的hash值为-1. 这里面定义的find的方法是从nextTable里进行查询节点,而不是以自身为头节点进行查找。
sizeCtl
控制标识符,用来控制table初始化和扩容操作的
含义
负数代表正在进行初始化或扩容操作
-1代表正在初始化
-N表示有N-1个线程正在扩容操作
正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小
类图
重要操作
initTable
ConcurrentHashMap初始化方法
只能有一个线程参与初始化操作,其他线程必须挂起
构造函数不做初始化过程,初始化真正真正是在put操作触发
步骤
sizeCtl<0 表示正在进行初始化,线程挂起
线程获取初始化资格(CAS( SIZECTL, sc, -1))进行初始化过程
初始化步骤完成后,设置sizeCtl=0.75*n (下一次扩容阈值),表示下一次扩容的大小
put
核心思想
根据hash值计算节点插入在table的位置,如果该位置为空,则直接插入,否则插入到链表或者树中。
在并发处理中使用的是乐观锁CAS,当有冲突的时候才进行并发处理Synchronized
步骤
如果没有初始化,table为null,线程进入初始化步骤,调用initTable()方法来进行初始化过程,如果有其它线程正在进行初始化,该线程挂起
如果插入的当前I位置为null,说明该位置是第一次插入,调用Unsafe的方法CAS插入节点即可;插入成功,则调用addCount判断是否需要扩容;插入失败,则继续匹配(自旋)
如果该节点的hash == MOVED(-1),表示有线程正在进行扩容,则调用helpTransfer()方法进入扩容进程中
helpTransfer()方法调用多个工作线程一起帮助进行扩容,这样的效率就会更高,而不是只有检查到要扩容的那个线程进行扩容操作,其他线程就要等待扩容操作完成才能工作
如果存在hash冲突,就加锁(Synchronized)来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入,
最后一个如果该链表的数量大于阈值8,就要先调用调用treeifyBin()将链表转换成黑红树的结构,break再一次进入循环
如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容
get
步骤
计算hash值,定位到该table索引位置,如果是首节点符合就返回
如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回
以上都不符合的话,就从链表、红黑树节点获取往下遍历节点,匹配就返回,否则最后就返回null(table==null; return null)
size
1.8中使用一个volatile类型的变量baseCount记录元素的个数,当插入新数据或则删除数据时,会通过addCount()方法更新baseCount(内部是CAS)
扩容
多线程扩容
场景
1. put方法触发:往hashMap中成功插入一个key/value节点时,有可能触发扩容动作:所在链表的元素个数达到了阈值 8,则会调用treeifyBin方法把链表转换成红黑树,不过在结构转换之前,会对数组长度进行判断,如果小于64,则优先扩容,而不是链表转树。
2. addCount方法触发:新增节点之后,会调用addCount方法记录元素个数,并检查是否需要进行扩容,当数组元素个数达到阈值时,会触发transfer方法,重新调整节点的位置。
步骤
步骤1
构建一个nextTable, 其大小为原来大小的两倍,这个步骤是在单线程环境下完成的
步骤2
将原来table里面的内容复制到nextTable中,这个步骤是允许多线程操作的
链表转为红黑树的过程
所在链表的元素个数得到了阈值8,则将链表转换为红黑树
红黑树算法
jdk 1.7
在JDK1.7中,ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成的,如图:
结构
示例图
Segment是一种可重入锁(ReentrantLock)
HashEntry是用于存储键值对的数据
HashEntry中的value以及next都被volatile修饰,这样在多线程读写过程中能够保持它们的可见性
对HashEntry数组的数据进行修改时,必须首先获得与它对应的Segment锁
重要操作
初始化
通过ConcurrentHashMap的初始化容量initialCapacity、每个segment的负载因子loadFactor、并发级别concurrencyLevel等几个参数来
初始化segments数组、段偏移量segmentShift、段掩码segmentMask和每个segment里的HashEntry数组来实现的
初始化Segments数组
segments数组的长度ssize是通过并发级别concurrencyLevel(最大值是65525)计算得出的,因按位与的散列算法定位segments数组的索引,故segments数组的长度是2的N次方
segments数组的长度ssize最大是65536,对应的二进制是16位
sshift等于ssize从1向左移动的位数,最大为16
segmentShift& segmentMask
段偏移量 segmentShift用于定位参与散列运算的位数,segmentShift等于32(ConcurrentHashMap中hash()方法输出的最大数是32位)减sshift,最大值是16
段掩码 segmentMask是散列运算的掩码,等于ssize减1,最大为65535
初始化每个segment
HashEntry数组的长度cap= log2 (initialCappacity/ssize)向上取整
Segment的容量threshold = (int) cap * loadFactor
定位Segment
再散列目的是减少冲突,使得元素能够均匀地分布在不同的segment上,从而提高容器的存取效率
get
一次再散列得到的散列值通过散列函数(得到值的高位)定位到segment,再通过散列算法定位到元素
get操作的高效之处在于整个get过程不需要加锁,除非读到的值是空才会加锁重读,因为get方法里将要使用的共享变量都定义成volatile类型
HashEntry中的value以及next都被volatile修饰,这样在多线程读写过程中能够保持它们的可见性
put
由于put方法需要对共享变量进行写入操作,所以为了线程安全,在操作共享变量时必须给Segment加锁
两大步骤
第一步判断是否需要对segment里的HashEntry数组进行扩容
第二步定位添加元素的位置,然后将其放在HashEntry数组里
size
统计整个ConcurrentHashMap里的元素大小,需要统计所有Segment里元素的大小后求和
核心实现
先尝试2次通过不锁Segment的方式来统计各个Segment大小,如果统计的过程中,容器的count发生了变换,则再采用给Segment加锁的方式来统计所有的大小
在put,remove,clean操作元素前都会将变量modCount加1,在统计size前后比较modCount大小是否发生变化,从而得知容器的大小count是否发生变化
扩容
区别于HashMap
Segment里的HashEntry数组先判断是否超过阈值|扩容,再插入元素
HashMap先插入元素后判断阈值
ConcurrentHashMap 不会对整个容器进行扩容,只会对某个segment进行扩容
HashMap会对整个容器进行扩容
首先会创建一个容量是原来容量两倍的数组,将原数组中的元素进行再散列后插入到新的数组里
1.8与1.7的区别
数据结构
JDK 1.7 数组+链表:Segment(ReentrantLock)+HashEntry
JDK 1.8 数组+链表/红黑树 Node(HashEntry) + Synchronized+CAS+红黑树
线程安全
JDK 1.7 Segment(ReentrantLock)
JDK 1.8 Synchronized+CAS
锁粒度
JDK1.7 锁的粒度是基于Segment的,包含多个HashEntry
JDK1.8 锁的粒度是基于Node的(HashEntry首节点),实现降低锁的粒度
查询时间复杂度
JDK1.7 遍历链表 O(N)
JDK1.8 遍历红黑树 O(log N)
链表转化为红黑树:定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。
辅助类
JDK 1.8 例如TreeBin,Traverser等对象内部类。
ConcurrentLinkedQueue
并发编程中需要用到线程安全的队列
1. 使用阻塞算法
1个锁(入队和出队用同一把锁)和2个锁(入队和出队用不同的锁)
阻塞队列
2. 使用非阻塞算法
CAS
ConcurrentLinkedQueue
结构
由head和tail节点组成,每个节点Node由节点元素item和指向下一个节点next的引用组成,他们四个都是volatile
默认情况下head节点存储的元素为空,tail节点等于head节点
基于链接节点的无边界的线程安全队列,采用FIFO原则对元素进行排序,内部采用CAS算法实现
重要操作
入队 offer()
入队列就是将入队节点添加到队列的尾部
3大步骤
1. 定位尾节点
tail节点不总是尾节点,通过tail节点来找到尾节点,尾节点可能是tail节点,也可能是tail节点的next节点
2. 设置入队节点为尾节点
使用CAS算法将入队节点设置成当前队列尾节点的下一个节点
3. 更新tail节点
如果tail节点的next不为空,则将入队节点设置成tail节点
如果tail节点的next节点为空,则将入队节点设置成tail节点的next节点
HOPS
tail节点不总是尾节点,提高入队的效率
减少CAS更新tail节点的次数能提高入队的效率,使用hops常量来控制并减少tail节点的更新频率,并不是每次节点入队后都将tail节点更新为尾节点,而是当tail节点和尾节点的距离大于等于常量HOPS的值(默认等于1)时才更新tail节点,
tail和尾节点的距离越长,使用CAS更新tail节点的次数就会少,但是距离越长带来的负面影响就是每次入队时定位尾节点的时间就越长,但这样仍然能提高入队的效率
因为本质上它通过增加对volatile变量的读操作来减少对volatile变量的写操作,而对volatile变量的写操作开销要远大于读操作,多以入队的效率会有所提升
注意:入队方法永远返回true,所哟不要通过返回值判断入队是否成功
出队 poll()
出队列就是从队列里返回一个节点元素,并清空该节点对元素的引用
HOPS
并不是每次出队时都更新head节点
当head节点里有元素时,直接弹出head节点里的元素,并不会更新head节点。
只有head节点里面没有元素时,出队操作才会更新head节点
这种做法通过hops变量来减少使用CAS更新head节点的消耗,从而提高出队效率
不变性
在入队的最后一个元素的next为null
队列中所有未删除的节点的item都不能为null且都能从head节点遍历到
对于要删除的节点,不是直接将其设置为null而是先将其item域设置为null(迭代器会跳过item为null的节点)
允许head和tail更新滞后。head、tail不总是指向第一个元素和最后一个元素
head的不变性和可变性
不变性
所有未删除的节点都可以通过head节点遍历到
head不能为null
head节点的next不能指向自身
可变性
head的item可能为null,也可能不为null
允许tail滞后head,也就是说调用succc()方法,从head不可达tail
tail的不变性和可变性
不变性
tail不能为null
可变性
tail的item可能为null,也可能不为null
tail节点的next域可以指向自身
允许tail滞后head,也就是说调用succc()方法,从head不可达tail
精妙之处:利用CAS来完成数据操作,同时允许队列的不一致性,弱一致性表现淋漓尽致。
ConcurrentSkipListMap
java里面的3 个 key-value数据结构
HashMap
Hash表:插入、查找最快,为O(1);如使用链表实现则可实现无锁;数据有序化需要显式的排序操作
TreeMap
红黑树:插入、查找为O(logn),但常数项较小;无锁实现的复杂性很高,一般需要加锁;数据天然有序
SkipList
Skip list(跳表)是一种可以代替平衡树的数据结构,默认是按照Key值升序的。
Skip List(跳跃表)是一种支持快速查找的数据结构,插入、查找和删除操作都仅仅只需要O(log n)对数级别的时间复杂度
SkipList让已排序的数据分布在多层链表中,以0-1随机数决定一个数据的向上攀升与否,通过“空间换时间”的一个算法,在每个节点中增加了向前的指针,在插入、删除、查找时可以忽略一些不可能涉及到的结点,从而提高了效率。
特性
由很多层结构组成,level是通过一定的概率随机产生的
每一层都是一个有序的链表,默认是升序,也可以根据创建映射时所提供的Comparator进行排序,具体取决于使用的构造方法
最底层(Level1)的链表包含所有元素
如果一个元素出现在Level i 的链表中,则它在Level I 之下的链表也都会出
每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下一层的元素
查找、删除、添加
实现
ConcurrentSkipListMap其内部采用SkipLis数据结构实现。
内部类
Node
Node表示最底层的单链表有序节点
key-value和一个指向下一个节点的next。
Index
Index表示为基于Node的索引层
Index提供了一个基于Node节点的索引Node,一个指向下一个Index的right,一个指向下层的down节点。
HeadIndex
HeadIndex用来维护索引层次
重要操作
put
首先通过findPredecessor()方法找到前辈节点Node
根据返回的前辈节点以及key-value,新建Node节点,同时通过CAS设置next
设置节点Node,再设置索引节点。采取抛硬币方式决定层次,如果所决定的层次大于现存的最大层次,则新增一层,然后新建一个Item链表。
最后,将新建的Item链表插入到SkipList结构中。
get
首先调用findPredecessor()方法找到前辈节点,然后顺着right一直往右找即可
remove
调用findPredecessor()方法找到前辈节点,然后通过右移,然后比较,找到后利用CAS把value替换为null
然后判断该节点是不是这层唯一的index,如果是的话,调用tryReduceLevel()方法把这层干掉,完成删除。
size
ConcurrentSkipListMap的size()操作和ConcurrentHashMap不同,它并没有维护一个全局变量来统计元素的个数,所以每次调用该方法的时候都需要去遍历。
调用findFirst()方法找到第一个Node,然后利用node的next去统计。最后返回统计数据,最多能返回Integer.MAX_VALUE。注意这里在线程并发下是安全的。
ConcurrentSkipListMap是通过HeadIndex维护索引层次,通过Index从最上层开始往下层查找,一步一步缩小查询范围,最后到达最底层Node时,就只需要比较很小一部分数据了。
ConcurrentSkipListSet
内部采用ConcurrentSkipListMap实现

6. 参考网址

  1. 参考来源:http://cmsblogs.com/wp-content/resources/img/sike-juc.png
  2. 《Java并发编程的艺术》_方腾飞PDF 提取码:o9vr
  3. http://ifeve.com/the-art-of-java-concurrency-program-1/
  4. Java并发学习系列-绪论
  5. Java并发编程实战
  6. 死磕 Java 并发精品合集

Java 并发系列之六:java 并发容器(4个)的更多相关文章

  1. java并发系列(八)-----java异步编程

    同步计算与异步计算 从多个任务的角度来看,任务是可以串行执行的,也可以是并发执行的.从单个任务的角度来看,任务的执行方式可以是同步的,也可以是异步的. Runnable.Callable.Future ...

  2. 【Java并发系列】--Java内存模型

    Java内存模型 1 基本概念 程序:代码,完成某一个任务的代码序列(静态概念) 进程:程序在某些数据上的一次运行(动态) 线程:一个进程有一个或多个线程组成(占有资源的独立单元) 2 JVM与线程 ...

  3. java并发系列(六)-----Java并发:volatile关键字解析

    在 Java 并发编程中,要想使并发程序能够正确地执行,必须要保证三条原则,即:原子性.可见性和有序性.只要有一条原则没有被保证,就有可能会导致程序运行不正确.volatile关键字 被用来保证可见性 ...

  4. ☕【Java深层系列】「并发编程系列」深入分析和研究MappedByteBuffer的实现原理和开发指南

    前言介绍 在Java编程语言中,操作文件IO的时候,通常采用BufferedReader,BufferedInputStream等带缓冲的IO类处理大文件,不过java nio中引入了一种基于Mapp ...

  5. 【java虚拟机系列】java虚拟机系列之JVM总述

    我们知道java之所以能够快速崛起一个重要的原因就是其跨平台性,而跨平台就是通过java虚拟机来完成的,java虚拟机属于java底层的知识范畴,即使你不了解也不会影响绝大部分人从事的java应用层的 ...

  6. Java Web系列:Java Web 项目基础

    1.Java Web 模块结构 JSP文件和AXPX文件类似,路径和URL一一对应,都会被动态编译为单独class.Java Web和ASP.NET的核心是分别是Servlet和IHttpHandle ...

  7. 【java多线程系列】java内存模型与指令重排序

    在多线程编程中,需要处理两个最核心的问题,线程之间如何通信及线程之间如何同步,线程之间通信指的是线程之间通过何种机制交换信息,同步指的是如何控制不同线程之间操作发生的相对顺序.很多读者可能会说这还不简 ...

  8. 【java开发系列】—— java输入输出流

    前言 任何语言输入输出流都是很重要的部分,比如从一个文件读入内容,进行分析,或者输出到另一个文件等等,都需要文件流的操作.这里简单介绍下reader,wirter,inputstream,output ...

  9. Java多线程系列一——Java实现线程方法

    Java实现线程的两种方法 继承Thread类 实现Runnable接口 它们之间的区别如下: 1)Java的类为单继承,但可以实现多个接口,因此Runnable可能在某些场景比Thread更适用2) ...

随机推荐

  1. 『optimization 动态规划』

    optimization Description \(visit\_world\) 发现有些优化问题可以用很平凡的技巧解决,所以他给你分享了这样一道题: 现在有一个长度为N的整数序列\(\{a_i\} ...

  2. WPF 精修篇 多属性触发器

    原文:WPF 精修篇 多属性触发器 多属性触发器就是多个属性都满足在触发 在属性触发器上加了一些逻辑判断 举栗子 这个栗子里  textBox 要满足俩个条件 才能触发背景变色 1)textbox的 ...

  3. 如何大批量的识别图片上的文字,批量图片文字识别OCR软件系统

    软件不需要安装,直接双击打开就可以用,废话不多说直接上图好了,方便说明问题 批量图片OCR(批量名片识别.批量照片识别等)识别,然后就下来研究了一下,下面是成果 使用步骤:打开单个图片识别,导入文件夹 ...

  4. MVC下通过jquery的ajax调用webapi

    如题 jquery的应用,不会的自己去补. 创建一个mvc项目,新建控制器.视图如下: 其中data控制器负责向前台提供数据,home控制器是一个简单的访问页控制器. data控制器代码如下: pub ...

  5. Mac系统docker初探

    最近把工作环境要切到mac中,由于一直想看看docker是怎么回事,以前在win和linux下面都没有用起来,这次在mac中决定试一把,尝试下新的环境部署方式. 安装docker mac中,直接有类似 ...

  6. ES6的常见语法!!

    let : 声明变量 不存在变量提前 拥有局部作用域 (只要有{}出现 则只在该{}范围内生效) (而var只在函数内会产生作用域范围) 不能重复声明 const : 声明常量(常量名从规范上来将 最 ...

  7. 10. Javascript 前后端数据加密

    为了加强项目的接口安全程度,需求如下 var options = { // 前端需要传送的数据加密 data: { abc: 123, bcd: 123, cds: '撒旦教付货款12313', }, ...

  8. WorkFlow二:简单的发邮件工作流

    1.使用事物代码SWDD.默认进入如下: 2.点击新建再点击转到抬头. 3.填写基础信息,工作流名称和描述.之后点击保存并返回. 这时候工作流的名字从之前的未命名改变了,工作流ID也根据上篇配置的前序 ...

  9. day 46

    目录 CSS样式操作 给字体设置长宽 字体颜色 语义 背景图片 边框 display 盒子模型 浮动(**************) 浮动带来的影响 clear overflow溢出属性 定位 位置的 ...

  10. 【转载】UNICODE与ASCII的区别

    原文地址:https://blog.csdn.net/lx697/article/details/5914417 最近的项目涉及到了国际化的问题,由于之前并没有接触到UNICODE编码,因此,在项目期 ...