zoukankan      html  css  js  c++  java
  • JAVA基础(9)——容器(3)——并发容器

    转载: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上每个节点中增加了向前的指针。

  • 相关阅读:
    图片和xml文件的转换
    WPF的样式(Style)继承
    .NET的序列化和反序列化
    WPF中的画板InkCanvas
    找到网页的源文件并找到歌曲文件的路径
    How to check if a ctrl + enter is pressed on a control?
    计算两个日期相差的天数
    图片保存到数据库以及从数据库中Load图片
    设计模式Command(命令模式)
    一个强大而且好用的UML设计工具
  • 原文地址:https://www.cnblogs.com/ceshi2016/p/8446769.html
Copyright © 2011-2022 走看看