JAVA基础(9)——容器(3)——并发容器
转载:http://blog.csdn.net/weitry/article/details/52964509
JAVA基础系列规划:
- JAVA基础(1)——基本概念
- JAVA基础(2)——数据类型
- JAVA基础(3)——容器(1)——常用容器分类
- JAVA基础(4)——容器(2)——普通容器
- JAVA基础(5)——并发(1)——总体认识
- JAVA基础(6)——并发(2)——原子
- JAVA基础(7)——并发(3)——锁机制
- JAVA基础(8)——并发(4)——线程池
- JAVA基础(9)——容器(3)——并发容器
- JAVA基础(10)——IO、NIO
- JAVA基础(11)——泛型
- JAVA基础(12)——反射
- JAVA基础(13)——序列化
- JAVA基础(14)——网络编程
在《 JAVA基础(3)——容器(1)——常用容器分类》对Java常用容器进行了一个分类。《 JAVA基础(4)——容器(2)——普通容器》介绍了普通容器,没有介绍并发容器。这篇文章将介绍并发容器,jdk8共提供了4类14个并发容器:
一、并发List
1. CopyOnWriteArrayList
CopyOnWriteArrayList源自jdk1.5,通常被认为是ArrayList的线程安全变体。内部由可变数组实现,和ArrayList的区别在于CopyOnWriteArrayList的数组内部均为有效数据。
可变性操作在添加或删除数据的时候,会对数组进行扩容或减容。扩容或减容的过程是:产生新数组,然后将有效数据复制到新数组,这也是“CopyOnWrite”的语义。但复制操作的效率比较低。
每次获取数组都是final类型的,数组引用不可变。同时在add、set、remove、clear、subList、sort等可变性操作内部加锁,保证了数组操作的线程安全性。get操作不加锁。
使用COWIterator进行遍历,内部为CopyOnWriteArrayList的数据数组的final快照,保证了遍历时数据的不变性。不支持remove操作。
综合上述特性,CopyOnWriteArrayList多线程安全,写操作复制和加锁导致效率较低,读操作序号读取效率高,适合使用在多线程、读操作远远大于写操作的场景里,比如缓存。
二、并发Queue
并发的Queue主要有4种共9个:
- BlockingQueue,包括ArrayBlockingQueue、DelayQueue、LinkedBlockingQueue、PriorityBlockingQueue和SynchronousQueue
- ConcurrentLinkedDeque
- LinkedBlockingDeque
- ConcurrentLinkedQueue
- LinkedTransferQueue
2. ArrayBlockingQueue
BlockingQueue源自jdk1.5,在Queue的基础上增加了2个操作:
- put操作,队列满时,存储元素的线程会阻塞,等待队列可用。
- take操作,队列为空时,获取元素的线程会阻塞,等待队列变为非空。
ArrayBlockingQueue是一个用数组实现的有界阻塞队列。内部有一个ReentrantLock是生产和消费公用的,保证线程安全。阻塞由两个Condition(notEmpty和notFull)控制。取数据时,队列空,则notEmpty.await();添加数据时,队列满,则notFull.await()。取出数据后,notFull.signal();;添加数据后,notEmpty.signal()。队列元素位置计数由变量takeIndex、putIndex和count控制。
默认情况下不保证访问者公平的访问队列,所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。我们可以使用以下代码创建一个公平的阻塞队列:
ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);
- 1
- 2
3. LinkedBlockingQueue
LinkedBlockingQueue源自jdk1.5,利用链表实现的有界阻塞队列,默认和最大长度为Integer.MAX_VALUE。生产和消费使用不同的锁(ReentrantLock takeLock和ReentrantLock putLock),对于put和offer采用一把锁,对于take和poll则采用另外一把锁,避免了读写时互相竞争锁的情况,分离了读写线程安全,因此LinkedBlockingQueue在高并发读写操作都多的情况下,性能会较ArrayBlockingQueue好很多,在遍历以及删除元素则要两把锁都锁住。
阻塞由两个Condition(notEmpty和notFull)控制。队列元素位置计数由变量(AtomicInteger count)控制。
put操作,在putLock锁内,若队列满,则阻塞notFull.await(),该阻塞在队列不满时由notFull.signal()唤醒。
take操作,在takeLock锁内,若队列空,则阻塞notEmpty.await(),该阻塞在队列非空时由notEmpty.signal()唤醒。
offer是无阻塞的enqueue或时间范围内阻塞enqueue,poll是无阻塞的dequeue或时间范围内阻塞dequeue。
4. DelayQueue
DelayQueue源自jdk1.5,是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现,优先队列的比较基准值是时间。队列中的元素必须实现Delayed接口,Delayed扩展了Comparable接口,比较的基准为延时的时间值,Delayed接口的实现类getDelay的返回值应为固定值(final)。在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。
具体实现为:当调用DelayQueue的offer方法时,把Delayed对象加入到优先队列中。DelayQueue的take方法,把优先队列的first拿出来(peek),如果没有达到延时阀值,则进行await处理。
我们可以将DelayQueue运用在以下应用场景:
- 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
- 定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。
5. SynchronousQueue
SynchronousQueue源自jdk1.5,是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。
可以认为SynchronousQueue是一个缓存值为1的阻塞队列,不能调用peek()方法来看队列中是否有数据元素,因为数据元素只有当你试着取走的时候才可能存在,不取走而只想偷窥一下是不行的,当然遍历这个队列的操作也是不允许的。 isEmpty()方法永远返回是true,remainingCapacity() 方法永远返回是0,remove()和removeAll() 方法永远返回是false,iterator()方法永远返回空,peek()方法永远返回null。
队列本身并不存储任何元素,非常适合于传递性场景,比如在一个线程中使用的数据,传递给另外一个线程使用,SynchronousQueue的吞吐量高于LinkedBlockingQueue 和 ArrayBlockingQueue。
SynchronousQueue的一个使用场景是在线程池里。Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收。
6. PriorityBlockingQueue
PriorityBlockingQueue源自jdk1.5,是一个按照优先级排列的阻塞队列,内部维护一个数组实现的平衡二叉树,里面存储的对象必须实现Comparable接口。队列通过这个接口的compare方法确定对象的优先级。
PriorityBlockingQueue队列添加新元素时候不是将全部元素进行顺序排列,而是从某个指定位置开始将新元素与之比较,一直比到队列头,这样既能保证队列头一定是优先级最高的元素。
每取一个头元素时候,都会对剩余的元素做一次调整,这样就能保证每次队列头的元素都是优先级最高的元素。
7. ConcurrentLinkedDeque
ConcurrentLinkedDeque源自jdk1.7,是一个非阻塞式并发双向无界队列,同时支持FIFO和FILO两种操作方式。
8. LinkedBlockingDeque
BlockingDeque源自jdk1.6,是一种阻塞式并发双向队列,同时支持FIFO和FILO两种操作方式。所谓双向是指可以从队列的头和尾同时操作,并发只是线程安全的实现,阻塞允许在入队出队不满足条件时挂起线程,这里说的队列是指支持FIFO/FILO实现的链表。
LinkedBlockingDeque源自jdk1.6,使用链表实现双向并发阻塞队列,根据构造传入的容量大小决定有界还是无界,默认不传的话,大小Integer.Max。
- 要想支持阻塞功能,队列的容量一定是固定的,否则无法在入队的时候挂起线程。也就是capacity是final类型的。
- 既然是双向链表,每一个结点就需要前后两个引用,这样才能将所有元素串联起来,支持双向遍历。也即需要prev/next两个引用。
- 双向链表需要头尾同时操作,所以需要first/last两个节点,当然可以参考LinkedList那样采用一个节点的双向来完成,那样实现起来就稍微麻烦点。
- 既然要支持阻塞功能,就需要锁和条件变量来挂起线程。这里使用一个锁两个条件变量来完成此功能。
由于采用一个独占锁,因此实现起来也比较简单。所有对队列的操作都加锁就可以完成。同时独占锁也能够很好的支持双向阻塞的特性。但由于独占锁,所以不能同时进行两个操作,这样性能上就大打折扣。从性能的角度讲LinkedBlockingDeque要比LinkedBlockingQueue要低很多,比CocurrentLinkedQueue就低更多了,这在高并发情况下就比较明显了。
9. ConcurrentLinkedQueue
ConcurrentLinkedQueue源自jdk1.5,是一种非阻塞式并发链表。采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素。
ConcurrentLinkedQueue由head节点和tair节点组成,每个节点(Node)由节点元素(item)和指向下一个节点的引用(next)组成,节点与节点之间就是通过这个next关联起来,从而组成一张链表结构的队列。默认情况下head节点存储的元素为空,tair节点等于head节点。
ConcurrentLinkedQueue则使用的wait-free算法解决并发问题。
10. LinkedTransferQueue
TransferQueue源自jdk1.7,是一种BlockingQueue,增加了transfer相关的方法。transfer的语义是,生产者会一直阻塞直到transfer到队列的元素被某一个消费者所消费(不仅仅是添加到队列里就完事)。使用put时不等待消费者消费。
LinkedTransferQueue采用的一种预占模式。意思就是消费者线程取元素时,如果队列为空,那就生成一个节点(节点元素为null)入队,然后消费者线程park住,后面生产者线程入队时发现有一个元素为null的节点,生产者线程就不入队了,直接就将元素填充到该节点,唤醒该节点上park住线程,被唤醒的消费者线程拿货走人。
LinkedTransferQueue使用链表实现TransferQueue接口。
三、并发Set
11. CopyOnWriteArraySet
CopyOnWriteArraySet源自jdk1.5,内部持有一个CopyOnWriteArrayList引用,所有操作都是基于对CopyOnWriteArrayList的操作。
12. ConcurrentSkipListSet
ConcurrentSkipListSet源自jdk1.6,内部持有ConcurrentSkipListMap,Set的数据value都被封装成< value, Boolean.TRUE>放入ConcurrentSkipListMap,所有操作都是基于对ConcurrentSkipListMap的操作。需要注意的是value必须是Comparable类型的。
四、并发Map
在jdk5之前,线程安全的Map内置实现只有Hashtable和Properties(注:不考虑Collections.synchronizedMap)。Properties基于Hashtable实现,前面已经讨论过Hashtable,已经过时,现在基本不再使用。jdk5开始,新增加了2个线程安全的Map:ConcurrentHashMap和ConcurrentSkipListMap。
13. ConcurrentHashMap
ConcurrentHashMap是HashMap的线程安全版本。
jdk8之前,ConcurrentHashMap使用锁分段技术,不仅保证了线程安全性,同时提高了并发访问效率。锁分段的原理是:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
ConcurrentHashMap实现时,由Segment数组和HashEntry数组组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。
jdk8开始,ConcurrentHashMap实现线程安全的思想完全改变,摒弃了Segment(锁段)的概念,启用CAS算法实现。它沿用了与它同时期的HashMap版本的思想,底层依然由“数组+链表+红黑树”的方式思想,但是为了做到并发,又增加了很多辅助的类,例如TreeBin、Traverser等内部类。
ConcurrentHashMap实现时,内部维护着一个table,里面存放着Node< K, V>,所有数据都在Node里面。Node和HashMap类型,差别在于Node对value和next属性设置了volatile同步锁,不允许调用setValue方法直接改变Node的value域,它增加了find方法辅助map.get()方法。put操作时,根据Key计算hash值,选择table中相应的Node,然后对Node加synchronized锁,将数据封装到Node中,插入到链表头部。如果该链表长度超过TREEIFY_THRESHOLD,将该链表上所有Node转换成TreeNode,并将该链表转换成TreeBin,由TreeBin完成对红黑树的包装,加入到table中。也就是说在实际的ConcurrentHashMap“数组”中,此位置存放的是TreeBin对象,而不是TreeNode对象,这是与HashMap的区别。
14. ConcurrentSkipListMap
ConcurrentSkipListMap是TreeMap的线程安全版本,使用CAS算法实现线程安全,适用于多线程情况下对Map的键值进行排序。
注:对于键值排序需求,非多线程情况下,应当尽量使用TreeMap;对于并发性相对较低的并行程序,可以使用Collections.synchronizedSortedMap将TreeMap进行包装,也可以提供较好的效率。对于高并发程序,应当使用ConcurrentSkipListMap,能够提供更高的并发度。和ConcurrentHashMap相比,ConcurrentSkipListMap 支持更高的并发。ConcurrentSkipListMap 的存取时间是log(N),和线程数几乎无关。也就是说在数据量一定的情况下,并发的线程越多,ConcurrentSkipListMap越能体现出他的优势。
ConcurrentSkipListMap由跳表(Skip list)实现,默认是按照Key值升序的。内部主要由Node和Index组成。同ConcurrentHashMap的Node节点一样,key为final,是不可变的,value和next通过volatile修饰保证内存可见性。Index封装了跳表需要的结构,首先node包装了链表的节点,down指向下一层的节点(不是Node,而是Index),right指向同层右边的节点。node和down都是final的,说明跳表的节点一旦创建,其中的值以及所处的层就不会发生变化(因为down不会变化,所以其下层的down都不会变化,那他的层显然不会变化)。
Skip list是一个”空间来换取时间”的算法:
1. 最底层(level1)是已排序的完整链表结构;
2. level1上元素以0-1随机数决定是否攀升到level2,同时level2上每个节点中增加了向前的指针;
3. level2上元素继续进行随机攀升到level3,并且level3上每个节点中增加了向前的指针。
JAVA基础(9)——容器(3)——并发容器的更多相关文章
- Java并发——同步容器与并发容器
同步容器类 早期版本的JDK提供的同步容器类为Vector和Hashtable,JDK1.2 提供了Collections.synchronizedXxx等工程方法,将普通的容器继续包装.对每个共有方 ...
- java多线程总结-同步容器与并发容器的对比与介绍
1 容器集简单介绍 java.util包下面的容器集主要有两种,一种是Collection接口下面的List和Set,一种是Map, 大致结构如下: Collection List LinkedLis ...
- Java并发—同步容器和并发容器
简述同步容器与并发容器 在Java并发编程中,经常听到同步容器.并发容器之说,那什么是同步容器与并发容器呢?同步容器可以简单地理解为通过synchronized来实现同步的容器,比如Vector.Ha ...
- Java基础技术多线程与并发面试【笔记】
Java基础技术多线程与并发 什么是线程死锁? 死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去,我们就可以称 ...
- Java并发(9)- 从同步容器到并发容器
引言 容器是Java基础类库中使用频率最高的一部分,Java集合包中提供了大量的容器类来帮组我们简化开发,我前面的文章中对Java集合包中的关键容器进行过一个系列的分析,但这些集合类都是非线程安全的, ...
- JAVA同步容器和并发容器
同步容器类 同步容器类的创建 在早期的JDK中,有两种现成的实现,Vector和Hashtable,可以直接new对象获取: 在JDK1.2中,引入了同步封装类,可以由Collections.sync ...
- Java编程的逻辑 (74) - 并发容器 - ConcurrentHashMap
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
- Java编程的逻辑 (73) - 并发容器 - 写时拷贝的List和Set
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
- 【Java并发编程二】同步容器和并发容器
一.同步容器 在Java中,同步容器包括两个部分,一个是vector和HashTable,查看vector.HashTable的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并 ...
- Java并发编程原理与实战三十三:同步容器与并发容器
1.什么叫容器? ----->数组,对象,集合等等都是容器. 2.什么叫同步容器? ----->Vector,ArrayList,HashMap等等. 3.在多线程环境下,为什么不 ...
随机推荐
- 2017-2018 ACM-ICPC Northern Eurasia (Northeastern European Regional) Contest (NEERC 17) 日常训练
A - Archery Tournament 题目大意:按时间顺序出现靶子和射击一个位置,靶子的圆心为(x, y)半径为r,即圆与x轴相切,靶子不会重叠,靶子被击中后消失, 每次射击找出哪个靶子被射中 ...
- POJ 2318 TOYS(点与直线的关系 叉积&&二分)
题目链接 题意: 给定一个矩形,n个线段将矩形分成n+1个区间,m个点,问这些点的分布. 题解: 思路就是叉积加二分,利用叉积判断点与直线的距离,二分搜索区间. 代码: 最近整理了STL的一些模板,发 ...
- 2018 ACM-ICPC 焦作网络赛
Problem A Problem B 简单题,做下背包就好了. Problem C Problem D Problem E Problem F Problem G Problem H Problem ...
- Python3 字典及三级菜单练习
#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Author;Tsukasa list_1 = { '广州':{ '越秀区':{ '五羊石像','镇海 ...
- python笔记三:函数式编程
1.概念: 函数式编程就是一种抽象程度很http://i.cnblogs.com/EditPosts.aspx?opt=1高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要 ...
- 总结分析Java常见的四种引用
从JDK1.2版本开始,把对象的引用分为四种级别,从而使程序能更加灵活的控制对象的生命周期.这四种级别由高到低依次为:强引用.软引用.弱引用和虚引用. 1.强引用 本章前文介绍的引用实际上都是强引用, ...
- 今天找到了关于用深度学习识别fre2013的代码
http://blog.csdn.net/walilk/article/details/58709611 http://blog.csdn.net/zwx2445205419/article/deta ...
- Linux前后台进程切换
(1).Linux前台进程与后台进程的区别 前台进程:是在终端中运行的命令,那么该终端就为进程的控制终端,一旦这个终端关闭,这个进程也随之消失. 后台进程:也叫守护进程(Daemon),是运行在后台的 ...
- Linux命令之route
route [-CFvnNee] [-A family] [-4|-6] route [-v] [-A family] [-4|-6] add [-net|-host] target [netmask ...
- [BZOJ2669][CQOI2012]局部最小值(容斥+状压DP)
发现最多有8个限制位置,可以以此为基础DP和容斥. $f_{i,j}=f_{i-1,j}\times (cnt_j-i+1)+\sum_{k\subset j} f_{i-1,k}$ $cnt_j$表 ...