转载:http://blog.csdn.net/weitry/article/details/52964509

JAVA基础系列规划:


《 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)——并发容器的更多相关文章

  1. Java并发——同步容器与并发容器

    同步容器类 早期版本的JDK提供的同步容器类为Vector和Hashtable,JDK1.2 提供了Collections.synchronizedXxx等工程方法,将普通的容器继续包装.对每个共有方 ...

  2. java多线程总结-同步容器与并发容器的对比与介绍

    1 容器集简单介绍 java.util包下面的容器集主要有两种,一种是Collection接口下面的List和Set,一种是Map, 大致结构如下: Collection List LinkedLis ...

  3. Java并发—同步容器和并发容器

    简述同步容器与并发容器 在Java并发编程中,经常听到同步容器.并发容器之说,那什么是同步容器与并发容器呢?同步容器可以简单地理解为通过synchronized来实现同步的容器,比如Vector.Ha ...

  4. Java基础技术多线程与并发面试【笔记】

    Java基础技术多线程与并发 什么是线程死锁? ​死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去,我们就可以称 ...

  5. Java并发(9)- 从同步容器到并发容器

    引言 容器是Java基础类库中使用频率最高的一部分,Java集合包中提供了大量的容器类来帮组我们简化开发,我前面的文章中对Java集合包中的关键容器进行过一个系列的分析,但这些集合类都是非线程安全的, ...

  6. JAVA同步容器和并发容器

    同步容器类 同步容器类的创建 在早期的JDK中,有两种现成的实现,Vector和Hashtable,可以直接new对象获取: 在JDK1.2中,引入了同步封装类,可以由Collections.sync ...

  7. Java编程的逻辑 (74) - 并发容器 - ConcurrentHashMap

    ​本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...

  8. Java编程的逻辑 (73) - 并发容器 - 写时拷贝的List和Set

    ​本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...

  9. 【Java并发编程二】同步容器和并发容器

    一.同步容器 在Java中,同步容器包括两个部分,一个是vector和HashTable,查看vector.HashTable的实现代码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并 ...

  10. Java并发编程原理与实战三十三:同步容器与并发容器

    1.什么叫容器? ----->数组,对象,集合等等都是容器.   2.什么叫同步容器? ----->Vector,ArrayList,HashMap等等.   3.在多线程环境下,为什么不 ...

随机推荐

  1. Python 2.7.13安装

    参考文章:安装Python 进入至Python官方网站,点击下载 下载完成后直接进行安装 选择安装的路径 选择安装的组件,请注意选择安装pip和Add python.exe to Path这两个选项 ...

  2. python版GetTickCount()

    time.clock() return the current processor time as a floating point number expressed in seconds. 即返回一 ...

  3. 215. Kth Largest Element in an Array【Medium】【找到第 k 大的元素】

    Find the kth largest element in an unsorted array. Note that it is the kth largest element in the so ...

  4. 如何判断图中存环(正&负)

    1.正环 用 SPFA不断的进行松弛操作,发现当前金额可以比本身大就更新,同时记录更新次数.如果更新次数超过n次,说明存在”正“环. 2.负环 这里先说明下负环.(求最短距离的时候) 在我们用SPFA ...

  5. bzoj 2286(虚树+树形dp) 虚树模板

    树链求并又不会写,学了一发虚树,再也不虚啦~ 2286: [Sdoi2011]消耗战 Time Limit: 20 Sec  Memory Limit: 512 MBSubmit: 5002  Sol ...

  6. JDK源码学习笔记——String

    1.学习jdk源码,从以下几个方面入手: 类定义(继承,实现接口等) 全局变量 方法 内部类 2.hashCode private int hash; public int hashCode() { ...

  7. [转]Web.xml配置详解之context-param

    转自:http://blog.csdn.net/liaoxiaohua1981/article/details/6759206 格式定义: [html] view plaincopy   <co ...

  8. HMAC的JAVA实现和应用

    1.简介: HMACSHA1 是从SHA1 哈希函数构造的一种键控哈希算法,被用作 HMAC(基于哈希的消息验证代码). 此 HMAC 进程将密钥与消息数据混合,使用哈希函数对混合结果进行哈希计算,将 ...

  9. svn安装和配置

    安装svn 参考http://blog.csdn.net/dl425134845/article/details/41978541 系统版本 uname -a # 查看内核/操作系统/CPU信息 he ...

  10. WPF中的动画——(四)缓动函数

    缓动函数可以通过一系列公式模拟一些物理效果,如实地弹跳或其行为如同在弹簧上一样.它们一般应用在From/To/By动画上,可以使得其动画更加平滑. var widthAnimation = new D ...