Java并发基础(14)—— 并发容器

一、背景

上一节我们学习了并发容器ConcurrentHashMap在JDK1.7中的实现,而在JDK1.8中,又有了一些区别

1、取消了segment数组,直接用table保存数据,锁的粒度更小,减少并发冲突的概率

2、存储数据时采用了链表+红黑树的形式,纯链表的形式时间复杂度为O(n),红黑树则为O(logn),性能提升很大。

     当key值相等的元素形成的链表中元素个数超过8个的时候,由链表转红黑树

二、数据结构

在jdk1.7中,采用segment数组和hashEntry数组

而在jdk1.8中,则采用Node数组,与hashEntry非常类似

三、初始化

在jdk1.7中,ConcurrentHashMap初始化时,会初始化Segment数组,实际上数组中只有第0个元素。同时,该元素中包含一个初始化好的table数组

而在jdk1.8中,ConcurrentHashMap初始化时并没有初始化table,只是给成员变量赋值,put时进行实际数组的填充

四、get操作

936行,获取hash值

937-938行,判断table数组中对应元素是否为空

939行,判断table数组中元素是否是我想要的,是就返回

943行,在红黑树中寻找

945行,在链表中寻找

五、put操作

首先看一下put方法,调用了putVal方法

1011行,参数判断

1012行,先获取hashCode,再调用spread方法进行再散列

1014行,for循环

1016-1017行,我们之前说过,ConcurrentHashMap初始化时并没有初始table。所以这里判断是否需要初始化table

这里注意2226行的sizeCtl

sizeCtl: 

负数:-1表示正在初始化

-N,表示有N-1个线程正在进行扩容

正数:0 表示还没有被初始化

>0的数,初始化或者是下一次进行扩容的阈值

2227行,当发现sizeCtl<0后,表明有其他线程正在初始化或者扩容操作,本线程调用yield方法让出cpu

2228行,多线程通过cas操作竞争,哪个线程将sizeCtl设置为-1,则进行初始化

2231-2238行,判断sc的值,并初始化好数组,同时设置扩容的阈值

至此,初始化好了Node数组。回到上一张图

1018行,如果table数组这个元素为null,直接把put的值加入数组

1023行,用于帮助扩容

1028行,判断是否是链表,是链表则加入,不是链表,则是红黑树

最后如果链表长度超过了8,则转换成红黑树(小于8时,红黑树会转换成链表)

这就是put方法的全部流程

六、其他并发容器

6.1 ConcurrentSkipListMap  和 ConcurrentSkipListSet

TreeMap和TreeSet有序的容器,这两种容器的并发版本

这里先了解一下跳表SkipList

以空间换时间,在原链表的基础上形成多层索引,但是某个节点在插入时,是否成为索引,随机决定,所以跳表又称为概率数据结构

6.2 ConcurrentLinkedQueue

无界非阻塞队列,底层是个链表,遵循先进先出原则。

add,offer将元素插入到尾部,peek(拿头部的数据,但是不移除)和poll(拿头部的数据,但是移除)

 6.3 写时复制容器 CopyOnWriteArrayList 、CopyOnWriteArraySet 

写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。

这样做的好处是我们可以对容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以写时复制容器也是一种读写分离的思想,读和写不同的容器。如果读的时候有多个线程正在向容器添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的,只能保证最终一致性,不能保证实时一致性。

适用读多写少的并发场景,常见应用:白名单/黑名单, 商品类目的访问和更新场景。

由于采用复制机制,存在内存占用问题。

读写锁可以实现类似效果,但是读写锁在写的时候,无法读。所以效率更低

6.4 阻塞队列

6.4.1 概念

1)当队列满的时候,插入元素的线程被阻塞,直达队列不满。

2)队列为空的时候,获取元素的线程被阻塞,直到队列不空。

6.4.2 用途

在生产者和消费者模式中,常会用到阻塞队列

生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这种生产消费能力不均衡的问题,便有了生产者和消费者模式。生产者和消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通信,而是通过阻塞队列来进行通信,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

6.4.3 常用阻塞队列

ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。

按照先进先出原则,要求设定初始大小

LinkedBlockingQueue:一个由链表结构组成的无界阻塞队列。

按照先进先出原则,可以不设定初始大小,Integer.Max_Value

ArrayBlockingQueue和LinkedBlockingQueue不同:

  1. 锁上面:ArrayBlockingQueue只有一个锁,LinkedBlockingQueue用了两个锁,
  2. 实现上:ArrayBlockingQueue直接插入元素,LinkedBlockingQueue需要转换。

PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。

默认情况下,按照自然顺序,要么实现compareTo()方法,指定构造参数Comparator

DelayQueue:一个使用优先级队列实现的无界阻塞队列。

支持延时获取的元素的阻塞队列,元素必须要实现Delayed接口。适用场景:实现自己的缓存系统,订单到期,限时支付等等。

SynchronousQueue:一个不存储元素的阻塞队列。

每一个put操作都要等待一个take操作

LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。

transfer(),必须要消费者消费了以后方法才会返回,tryTransfer()无论消费者是否接收,方法都立即返回。

LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

可以从队列的头和尾都可以插入和移除元素,实现工作密取,方法名带了First对头部操作,带了last从尾部操作,另外:add=addLast; remove=removeFirst; take=takeFirst

常用方法

方法

抛出异常

返回值

一直阻塞

超时退出

插入方法

add

offer

put

Offer(time)

移除方法

remove

poll

take

Poll(time)

检查方法

element

peek

N/A

N/A

  1. 抛出异常:当队列满时,如果再往队列里插入元素,会抛出IllegalStateException("Queuefull")异常。当队列空时,从队列里获取元素会抛出NoSuchElementException异常。
  2. 返回特殊值:当往队列插入元素时,会返回元素是否插入成功,成功返回true。如果是移除方法,则是从队列里取出一个元素,如果没有则返回null。
  3. 一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断退出。当队列空时,如果消费者线程从队列里take元素,队列会阻塞住消费者线程,直到队列不为空。
  4. 超时退出:当阻塞队列满时,如果生产者线程往队列里插入元素,队列会阻塞生产者线程一段时间,如果超过了指定的时间,生产者线程就会退出。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值