并发编程学习笔记 之 常用并发容器的概念及使用方法

1、概念

  并发容器是指在高并发应用程序的使用过程中,这些容器(数据结构)是线程安全的,而且在高并发的程序中运行它们会有高效的性能表现。

2、BlockingQueue(阻塞队列)

  Blocking Queue是指其中的元素数量存在界限,当队列已满时(队列元素数量达到了最大容量的临界值),对队列进行写入操作的线程将被阻塞挂起,当队列为空时(队列元素数量达到了为0的临界值),对队列进行读取的操作线程将被阻塞挂起。BlockingQueue(LinkedTransferQueue除外)的内部实现主要依赖于显式锁Lock及其与之关联的Condition。
并发编程学习笔记 之 常用并发容器的概念及使用方法_第1张图片
并发编程学习笔记 之 常用并发容器的概念及使用方法_第2张图片

2.1、ArrayBlockingQueue

  ArrayBlockingQueue是一个基于数组结构实现的FIFO阻塞队列,在构造该阻塞队列时需要指定队列中最大元素的数量(容量)。当队列已满时,若再次进行数据写入操作,则线程将会进入阻塞,一直等待直到其他线程对元素进行消费。当队列为空时,对该队列的消费线程将会进入阻塞,直到有其他线程写入数据。

  常用方法:

  1. void put(E e):向队列的尾部插入新的数据,当队列已满时调用该方法的线程会进入阻塞,直到有其他线程对该线程执行了中断操作,或者队列中的元素被其他线程消费。
  2. boolean offer(E e, long timeout, TimeUnit unit):向队列尾部写入新的数据,当队列已满时执行该方法的线程在指定的时间单位内将进入阻塞,直到到了指定的超时时间后,或者在此期间有其他线程对队列数据进行了消费、或者被其他线程中断,就可以使当前线程退出阻塞。该方法的返回值boolean为true时表示写入数据成功,为false时表示写入数据失败。
  3. boolean add(E e):向队列尾部写入新的数据,当队列已满时不会进入阻塞,但是该方法会抛出队列已满的异常。
  4. boolean offer(E e):向队列尾部写入新的数据,当队列已满时不会进入阻塞,并且会立即返回false。
  5. E take():从队列头部获取数据,并且该数据会从队列头部移除,当队列为空时执行take方法的线程将进入阻塞,直到有其他线程写入新的数据,或者当前线程被执行了中断操作。
  6. E poll(long timeout, TimeUnit unit):从队列头部获取数据并且该数据会从队列头部移除,如果队列中没有任何元素时则执行该方法,当前线程会阻塞指定的时间,直到在此期间有新的数据写入,或者阻塞的当前线程被其他线程中断,当线程由于超时退出阻塞时,返回值为null。
  7. E poll():从队列头部获取数据并且该数据会从队列头部移除,当队列为空时,该方法不会使得当前线程进入阻塞,而是返回null值。
  8. E peek():peek的操作类似于debug操作,它直接从队列头部获取一个数据,但是并不能从队列头部移除数据,当队列为空时,该方法不会使得当前线程进入阻塞,而是返回null值。
2.2、PriorityBlockingQueue

  PriorityBlockingQueue优先级阻塞队列是一个“无边界”阻塞队列。该队列会根据某种规则(Comparator)对插入队列尾部的元素进行排序,因此该队列将不会遵循FIFO(first-in-first-out)的约束。

  1. 排序且无边界的队列
    PriorityBlockingQueue存放数据的数量是“无边界”的,在PriorityBlockingQueue内部维护了一个Object的数组,随着数据量的不断增多,该数组也会进行动态地扩容。在构造PriorityBlockingQueue时虽然提供了一个整数类型的参数,但是该参数所代表的含义与ArrayBlockingQueue完全不同,前者是构造PriorityBlockingQueue的初始容量,后者指定的整数类型参数则是ArrayBlockingQueue的最大容量。同时,该队列只支持实现了Comparable接口的数据类型,默认情况下,优先级最小的数据元素将被放在队列头部,优先级最大的数据元素将被放在队列尾部。
  2. 不存在阻塞写方法
    由于PriorityBlockingQueue是“无边界”的队列,因此将不存在对队列上限临界值的控制,在PriorityBlockingQueue中,添加数据元素的所有方法都等价于offer方法,从队列的尾部添加数据,但是该数据会根据排序规则对数据进行排序。
  3. 优先级队列读方法
    优先级队列添加元素的方法不存在阻塞(由于是“无边界”的),但是针对优先级队列元素的读方法则与ArrayBlockingQueue类似。
2.3、LinkedBlockingQueue

  LinkedBlockingQueue是“可选边界”基于链表实现的FIFO队列。LinkedBlockingQueue队列的边界可选性是通过构造函数来决定的,当我们在创建LinkedBlockingQueue对象时,使用的是默认的构造函数,那么该队列的最大容量将为Integer的最大值(所谓的“无边界”),当然也可以通过指定队列最大容量(有边界)的方式创建队列。

2.4、DelayQueue

  DelayQueue也是一个实现了BlockingQueue接口的“无边界”阻塞队列,对于存入DelayQueue中的元素是有一定要求的:元素类型必须是Delayed接口的子类,存入DelayQueue中的元素需要重写getDelay(TimeUnit unit)方法用于计算该元素距离过期的剩余时间,如果在消费DelayQueue时发现并没有任何一个元素到达过期时间,那么对该队列的读取操作会立即返回null值,或者使得消费线程进入阻塞。在DelayQueue中,元素也会根据优先级进行排序,这种排序可以是基于数据元素过期时间而进行的。

2.5、SynchronousQueue

  SynchronousQueue也是实现自BlockingQueue的一个阻塞队列,每一次对其的写入操作必须等待(阻塞)其他线程进行对应的移除操作,SynchronousQueue的内部并不会涉及容量、获取size,就连peek方法的返回值永远都将会是null,除此之外还有更多的方法在SynchronousQueue中也都未提供对应的支持。
  尽管SynchronousQueue是一个队列,但是它的主要作用在于在两个线程之间进行数据交换,区别于Exchanger的主要地方在于(站在使用的角度)SynchronousQueue所涉及的一对线程一个更加专注于数据的生产,另一个更加专注于数据的消费,而Exchanger则更加强调一对线程数据的交换。

2.6、LinkedBlockingDeque

  LinkedBlockingDeque是一个基于链表实现的双向(Double Ended Queue,Deque)阻塞队列,双向队列支持在队尾写入数据,读取移除数据;在队头写入数据,读取移除数据。LinkedBlockingDeque实现自BlockingDeque(BlockingDeque又是BlockingQueue的子接口),并且支持可选“边界”,与LinkedBlockingQueue一样,对边界的指定在构造LinkedBlockingDeque时就已经确定了。

2.7、LinkedTransferQueue

  TransferQueue是一个继承了BlockingQueue的接口,并且增加了若干新的方法。LinkedTransferQueue是TransferQueue接口的实现类,其定义为一个无界的队列,具有FIFO的特性。

3、ConcurrentQueue(并发队列)

  在前面学习的BlockingQueue阻塞队列,为了保护共享数据的一致性,需要对共享数据的操作进行加锁处理(显式锁或者synchronized关键字),为了使得操作线程挂起和被唤醒,我们需要借助于对象监视器的wait/notify/notifyAll或者与显式锁关联的Condition。而ConcurrentQueue(并发队列)是一种可以不用关心临界值,操作该队列的线程也不会被挂起并且等待被其他线程唤醒,我们可以向该队列中插入或者获取数据,并且该队列是线程安全的。

  • ConcurrentLinkedQueue:无锁的、线程安全的、性能高效的、基于链表结构实现的FIFO单向队列(在JDK1.5版本中被引入)。
  • ConcurrentLinkedDeque:无锁的、线程安全的、性能高效的、基于链表结构实现的双向队列(在JDK1.7版本中被引入)。

注意:

  1. 在并发队列中使用size方法不是个好主意
  2. ConcurrentLinkedQueue的内存泄漏问题

4、ConcurrentMap(并发映射)

  Hashtable或者SynchronizedMap虽然是线程安全的,但是在多线程高并发的环境中,简单粗暴的排他式加锁方式效率并不是很高。鉴于Map是一个在高并发的应用环境中应用比较广泛的数据结构,自JDK 1.5版本起在Java中引入了ConcurrentHashMap并且在随后的JDK版本迭代中都在不遗余力地为性能提升做出努力,除了ConcurrentHashMap之外,在JDK 1.6版本中又引入了另外一个高并发Map的解决方案ConcurrentSkipListMap。

ConcurrentHashMap

  ConcurrentHashMap是一个支持高并发更新与查询的哈希表(基于HashMap)。在保证安全的前提下,进行检索不需要锁定。与hashtable不同,该类不依赖于synchronization去保证线程操作的安全。

ConcurrentSkipListMap

  ConcurrentSkipListMap提供了一种线程安全的并发访问的排序映射表。内部是SkipList(跳表)结构实现,在理论上,其能够在O(log(n))时间内完成查找、插入、删除操作。

5、写时拷贝算法(Copy On Write)

  CopyOnWrite容器,简称COW,该容器的基本实现思路是在程序运行的初期,所有的线程都共享一个数据集合的引用。所有线程对该容器的读取操作将不会对数据集合产生加锁的动作,从而使得高并发高吞吐量的读取操作变得高效,但是当有线程对该容器中的数据集合进行删除或增加等写操作时才会对整个数据集合进行加锁操作,然后将容器中的数据集合复制一份,并且基于最新的复制进行删除或增加等写操作,当写操作执行结束以后,将最新复制的数据集合引用指向原有的数据集合,进而达到读写分离最终一致性的目的。
  CopyOnWrite容器是一种读写分离的思想,读和写不同的容器,因此不会存在读写冲突,而写写之间的冲突则是由全局的显式锁Lock来进行防护的,因此CopyOnWrite常常被应用于读操作远远高于写操作的应用场景中

  Java中提供了两种CopyOnWrite算法的实现类:

  • CopyOnWriteArrayList:在JDK1.5版本被引入,用于高并发的ArrayList解决方案,在某种程度上可以替代Collections.synchronizedList。
  • CopyOnWriteArraySet:也是自JDK1.5版本被引入,提供了高并发的Set的解决方案,其实在底层,CopyOnWriteArraySet完全是基于CopyOnWriteArrayList实现的。

COW算法为解决高并发读操作提供了一种新的思路(读写分离),但是其仍然存在一些天生的缺陷,具体如下:

  1. 数组复制带来的内存开销:因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的数据集合和新拷贝的数据集合,当然旧的数据集合在拷贝结束以后会满足被回收的条件,但是在某个时间段内,内存还是会有将近一半的浪费。
  2. CopyOnWrite并不能保证实时的数据一致性:CopyOnWrite容器只能保证数据的最终一致性,并不能保证数据的实时一致性

你可能感兴趣的:(笔记,学习,java,数据结构)