Java多线程技术之九(JUC之并发容器)

一、简介

java的集合框架中,容器主要分为List、Set、Queue、Map四大类,常用的容器ArrayList、LinkedList、HashSet、HashMap等都不是线程安全的,在多线程环境下使用这些容器需要我们进行同步处理。为了简化开发,Java提供了同步容器。

同步容器

简单地在在需要同步的方法上添加synchronized来实现同步的容器。主要的同步容器:

  • Vector
  • Stack
  • Hashtable
  • Collections.SynchronizedList()方法创建的List
  • Collections.synchronizedSet()方法创建的Set
  • Collections.synchronizedMap()方法创建的Map

同步容器的问题
同步容器都是以自身为锁,多个线程共同竞争容器的锁时,吞吐量降低。因此,在并发量比较高的场景,可以使用并发容器来替代。

并发容器

java.util.concurrent包中针对并发环境设计的容器。并发容器使用了写时复制、分段锁、CAS等技术,在并发环境下能提供更高的吞吐量。

二、并发容器结构图

并发容器结构图.png

三、并发List

CopyOnWriteArrayList

对应ArrayList,使用方法和ArrayList相同。内部实现基于写入时复制技术。

写入时复制(Copy-On-Write)

是计算机程序设计领域中的一种优化思想,是一种读写分离思想,在很多系统设计上都会用到,常用于读多写少的并发场景。Copy-On-Write的原理是,对于查询操作,不需要加锁,直接访问数据。对于写入(增删改)操作,需要对操作加互斥锁,保证只会有1个线程在操作。在操作数据前,先将数据复制一份副本,之后的操作是对副本进行的。操作完成后,将原数据的指针指向副本,完成了副本替换原数据。因此,写入操作的同时,读操作不会被阻塞,而是继续读取原数据。

Copy-On-Write存在的问题
内存占用:由于有复制操作存在,在写操作比较频繁或者数据比较大的场景,Copy-On-Write比较耗存。
数据一致性:写副本完成到原数据指针被替换之间,如果有读操作,则读到的是旧数据。Copy-On-Write只能保证数据的最终一致性,不能保证数据的实时一致性。

四、并发Set

CopyOnWriteArraySet

对应HashSet,使用方法和HashSet相同。内部实现基于CopyOnWriteArrayList。

ConcurrentSkipListSet

对应TreeSet,使用方法和TreeSet相同。内部实现基于ConcurrentSkipListMap。

五、并发Queue

Queue接口表示单向队列,单向队列只能在队列头部删除元素、在队列末尾添加元素。

Queue的核心方法:

  • offer:将元素加入队列的末尾。
  • poll: 将元素从队列的头部删除。
  • peek:返回队列头部的元素,但不进行删除。
ConcurrentLinkedQueue

ConcurrentLinkedQueue类实现了Queue接口。表示基于链表的单向队列,它的实现使用了CAS无锁算法。

BlockingQueue接口

BlockingQueue接口扩展了Queue接口。表示阻塞单向队列。

阻塞队列和生产者-消费者模型
阻塞队列的工作模式是:当队列满时,队列会阻塞插入元素的线程,直到队列不满;当队列为空时,队列会阻塞获取元素的线程,直到队列不空。
阻塞队列常用于生产者-消费者模型,生产者向队列中添加元素,消费者则从队列中取出元素。线程池就是利用阻塞队列来实现任务的排队。

Queue的核心方法:

  • put:将元素加入队列的末尾,如果队列已满,线程将被阻塞。
  • take:将元素从队列的头部删除,如果队列为空,线程将被阻塞。
BlockingQueue的五个实现类
  • LinkedBlockingQueue
    基于链表的阻塞队列,它的容量没有上限,可以指定其最大容量。

  • ArrayBlockingQueue
    基于数组的阻塞循环队列,在构造时需要指定最大容量。

  • PriorityBlockingQueue
    基于堆的阻塞优先级队列,它的容量没有上限。优先级队列的元素按优先级顺序出队,而不是先进先出。可以自定义compareTo()方法来指定元素的优先级排序规则。

  • DelayQueue
    基于堆的阻塞时间调度队列,它的容量没有上限。此队列中的元素必须实现Delayed接口,在创建元素时可以指定延迟时间,只有在延迟时间满后,才能从队列中获取元素。队列头部是延迟时间满后保存时间最长的元素。如果延迟时间都还没满,则队列没有头部。此队列不允许使用null元素。DelayQueue多应用于定时任务调度等场景。

  • SynchronousQueue
    不存储元素的阻塞队列。此队列每一个put操作必须等待一个take操作,否则不能继续添加元素,队列本身不存储元素,看起来像是一根管道,资源从一个方快速传递到另一方。还可以指定队列的公平性,默认非公平。公平的队列可保证线程以FIFO的顺序进行访问,但是公平通常会降低吞吐量。SynchronousQueue非常适合传递性场景,即把生产者线程处理的数据直接传递给消费者线程。

TransferQueue接口

TransferQueue接口扩展了BlockingQueue接口。它提供了线程之间直接交换对象的能力。TransferQueue是一个结合了ConcurrentLinkedQueue, SynchronousQueue(公平模式)和LinkedBlockingQueues特性的组合体,具有较高的性能,推荐使用。

TransferQueue的核心方法:

  • tryTransfer(E e)
    若当前存在一个正在等待获取的消费者线程,则立刻移交元素e。否则,返回false。
  • transfer(E e)
    若当前存在一个正在等待获取的消费者线程,则立刻移交元素e。否则,插入当前元素e到队列尾部,并且当前线程进入阻塞状态,直到有消费者线程取走该元素。
  • hasWaitingConsumer()
    如果至少有一位消费者在等待,则返回true。
  • getWaitingConsumerCount()
    返回等待的消费者数量的估计值。
LinkedTransferQueue

LinkedTransferQueue实现了TransferQueue接口。表示基于链表的阻塞队列,它的容量没有上限。当有消费者等待获取元素时,可以通过transfer()方法把生产者传入的元素立即传给消费者。

LinkedTransferQueue采用了一种预占模式。当消费者取元素时,如果队列为空,则生成一个元素为null的节点并入队,然后消费者等待在这个节点上。当生产者在插入元素时发现队列中有一个元素为null的节点,于是生产者就不入队了,直接将元素填充到该节点并唤醒在此节点等待的消费者,消费者醒来后取走元素。如果当前没有等待获取的消费者,则生产者在插入元素后会一直等待直到插入的元素被消费者取走。TransferQueue不会让生产者过度生产,生产者的生产速度取决于消费者的消费能力,因此不会造成OutOfMemory错误。

六、并发Deque

Deque扩展了Queue接口,表示双向队列,双向队列可以在首尾两端访问、插入或删除元素。当只从队列一端进行入队和出队时即为栈。

Deque作为单向队列:

  • offer:将元素加入队列的末尾。
  • poll:将元素从队列的头部删除。
  • peek:返回队首的元素,但不进行删除。

Deque作为双向队列:

  • offerFirst: 添加元素到头部。如果队列已满,则返回false。
  • offerLast: 添加元素到尾部。如果队列已满,则返回false。
  • pollFirst:将元素从队列的头部删除。
  • pollLast:将元素从队列的末尾删除。
  • peekFirst:返回队列头部的元素,但不进行删除。
  • peekLast:返回队列末尾的元素,但不进行删除。

Deque作为栈:

  • push:将元素入栈。
  • pop:将元素出栈。
  • peek:返回栈顶元素,但不进行删除。
ConcurrentLinkedDeque

ConcurrentLinkedDeque实现了Deque接口。表示基于链表的双向队列。使用了CAS的非阻塞算法来保证线程并发访问时的数据一致性。

BlockingDeque接口

BlockingDeque接口扩展了BlockingQueue接口。表示阻塞双向队列。

BlockingDeque的核心方法:

  • putFirst: 添加元素到头部。如果队列已满,线程将被阻塞。
  • putLast: 添加元素到尾部。如果队列已满,线程将被阻塞。
  • takeFirst: 移除头部元素。如果队列为空,线程将被阻塞。
  • takeLast: 移除尾部元素。如果队列为空,线程将被阻塞。
LinkedBlockingDeque

LinkedBlockingDeque实现了BlockingDeque接口。表示基于链表的双向阻塞队列。

七、并发Map

ConcurrentHashMap

对应HashMap,使用方法和HashMap相同。内部实现基于分段锁技术,JDK8中改为CAS无锁算法。

ConcurrentSkipListMap

对应TreeMap,使用方法和TreeMap相同。内部实现基于跳跃表(Skip-List)和CAS无锁算法。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342