程序员最近都爱上了这个网站  程序员们快来瞅瞅吧!  it98k网:it98k.com

本站消息

站长简介/公众号

  出租广告位,需要合作请联系站长


+关注
已关注

分类  

暂无分类

标签  

暂无标签

日期归档  

2023-06(4)

java 并发容器

发布于2021-06-12 13:54     阅读(735)     评论(0)     点赞(18)     收藏(4)


什么是并发容器

开发中我们常用的数据结构有四大类别:List、Set、Queue、Map,这四大类中常用的是ArrayList、LinkedList、HashMap等,这些容器都是非线程安全的。在并发情况下,会有各种问题,如果想要保证线程安全,那么需要进行加锁操作,使用起来非常不方便,这种情况下,也就开发出了专门在并发情况下使用的容器。

并发容器概览

  • ConcurrentHashMap:线程安全的HashMap
  • CopyOnwriteArrayList:线程安全的List
  • BlockingQueue:这是一个接口,表示阻塞队列,非常适合用于作为数据共享的通道
  • ConcurrentLinkedQueue:高效的非阻塞并发队列,使用链表实现,可以看做一个线程安全的LinkedList
  • ConcurrentSkipListMap:是一个Map,使用跳表的数据结构进行快速查找

过时的并发容器

  • Vector和HashTable
    Vector和HashTable只是在原有数据结构上(ArrayList和HashMap的基础上加了synchronized关键字,效率很低)
  • HashMap和ArrayList
    HashMap和ArrayList虽然不是线程安全的,但是可以用Collections.synchronizedList(new ArrayList())和Collections.synchronizedMap(new HashMap<K,V>())使其变成线程安全的,不过效率也比较低。

为什么HashMap是线程不安全的?

  • 同时put碰撞导致数据丢失
    在多线程同时操作HashMap时,如果计算出的hashcode一样,就只会保留一个,另一个数据就会丢失
  • 同时put扩容导致数据丢失
    在多线程同时操作HashMap时,如果同时put,同时扩容,也只有一个会保留下来
  • 死循环造成的CPU100%
    主要存在JDK7及以前
    原因:在多个线程同时扩容的时候,会造成链表的死循环,也就是你指向我,我指向你的情况

JDK1.7的ConcurrentHashMap实现和分析

  • java7中的ConcurrentHashMap最外层是多个segment,每个segment的底层数据结构都和HashMap类似,仍然是数组和链表组成的拉链法
  • 每个segment独立上ReentrantLock锁,每个segement之间互不影响,因此提高了并发效率
  • ConcurrentHashMap默认有16个Segments,所以最多可以同时支持16个线程并发写(操作分别分布在不同的Segment上)。这个默认值可以在初始化的时候设置为其他值,但是一旦初始化以后,是不可以扩容的。

在这里插入图片描述
在这里插入图片描述

ConcurrentHashMap

  • putVal流程

1.判断key value不为空
2.计算hash值
3.根据对应位置节点的类型,来赋值,或者helptransfer,或者增长链表,或者给红黑树增加节点
4.检查满足阀值就“红黑树化”
5.返回oldVal

  • get流程

1.计算hash值
2.找到对应的位置,根据情况进行:
3.直接取值
4.红黑树里找值
5.遍历链表取值
6.返回找到的结果

  • 为什么超过8就要转为红黑树?
  • 默认链表结构是因为链表所占内存较少
  • 想要达到hashcode冲突为8是很难得,将近千万分之一,万一真的发生了这种情况,将链表转化为红黑树是可以保证其查询效率的
    在这里插入图片描述
    源码里也注释出了链表长度为8发生的概率

CopyOnWriteArrayList

  • 代替Vector和SynchronizedList,就和ConcurrentHashMap代替SynchronizedMap的原因一样
  • Vector和SynchronizedList的锁的粒度太大,并发效率相对比较低,并且迭代时无法编辑
  • Copy-On-Write并发容器还包括CopyOnWriteArraySet,用来代替同步Set
CopyOnWriteArrayList适用场景
  • 读操作可以尽可能地快,而写即使慢一些也没有太大关系
  • 读多写少:例如黑名单,每日更新;监听器:迭代操作远多于修改操作
CopyOnWriteArrayList读写规则
  • 回顾读写锁:读读共享、其他都互斥(写写互斥、读写互斥、写读互斥)
  • 读写锁规则的升级:读取是完全不用加锁的,并且更厉害的是,写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待,也就是说读写可以兼容,只有写写才会等待
CopyOnWriteArrayList实现原理
  • 创建新副本、读写分离
  • 写的时候会创建一个新的容器,读的时候会在旧的容器里读,因此读写互不相干,哪怕写入了新的值,但是读取的时候还是会读最开始的那些数据,等写完之后,再将老容器的数据指向新的容器,旧容器就作废了,等于做了新旧数据的替换
CopyOnWriteArrayList的缺点
  • 数据一致性问题:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的数据,马上能读到,就不能使用CopyOnWrite容器。
  • 内存占用问题:因为CopyOnWrite的写是复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,所以比较占用内存。
源码分析

在这里插入图片描述
源码里可以看到,就是一个array,里面应用了ReentrantLock来保证线程安全
在这里插入图片描述

并发队列Queue:阻塞队列

并发队列关系图
在这里插入图片描述

阻塞队列BlockingQueue
什么是阻塞队列?

1.阻塞队列是具有阻塞功能的队列,所以它首先是一个队列,其次是具有阻塞功能。
2.通常,阻塞队列的一端是给生产者放数据用,另一端给消费者拿数据用。阻塞队列是线程安全的,所以生产者和消费者都可以是多线程的。

  • take()方法:获取并移除队列的头结点,一旦如果执行take的时候,队列里无数据,则阻塞,知道队列里有数据
  • put()方法:插入元素。但是如果队列已满,那么就无法继续插入,则阻塞,直到队列里有了空闲空间
  • 是否有界(容量有多大):这是一个非常重要的属性,无界队列意味着里面可以容纳非常多(Integer.MAX_VALUE,约为2的31次方,是一个非常大的数值,可以近似认为是一个无限容量)
  • 阻塞队列和线程池的关系:阻塞队列是线程池的重要组成部分
  • put ,take:
    put:添加数据,如果队列满了则会阻塞
    take:和put类似,只不过是取值
  • add,remove,element:如果队列满了,无论是添加还是取出值,都会抛异常
  • offer:放入值,并将结果返回,为boolean类型
    poll:取出并将该值删除,如果没有就返回null
    peek:取出该值,但是不删除
ArrayBlockingQueue
  • 有界
  • 指定容量
  • 公平:还可以指定是否需要保证公平,如果想保证公平的话,那么等待了最长时间的线程会被优先处理,不过这会同时带来一定的性能损耗
LinkedBlockingQueue
  • 无界
  • 容量Integer.MAX_VALUE
  • 内部结构:Node、两把锁(take、put都加了锁)。
PriorityBlockingQueue
  • 支持优先级
  • 自然顺序(而不是先进先出)
  • 无界队列
  • PriorityQueue的线程安全版本
    proirityBlockingQueue是一个无界队列,他没有限制,在内存允许的情况下可以无限添加元素;他又是具有优先级的队列,是通过构造函数传入的对象来判断,传入的对象必须实现comparable接口。
SynchronousQueue
  • 他的容量为0
  • 需要注意的是,SynchronousQueue的容量不是1而是0,以为SynchronousQueue不需要去持有元素,他所作的就是直接传递
  • 效率很高
DelayQueue
  • 延迟队列,根据延迟时间排序
  • 元素需要实现Delayed接口,规定排序规则
    DelayQueue内部封装了一个PriorityQueue,他会根据time的先后时间排序(time小的排在前面);DelayQueue也是一个无边界队列

非阻塞并发队列

-并发包中的非阻塞队列只有ConcurrentLinkedQueue这一种,顾名思义ConcurrentLinkedQueue是使用链表作为其数据结构的,使用CAS非阻塞算法来实现线程安全(不具备阻塞功能),适合用在对性能要求较高的并发场景。用的相对比较少一些

  • 看源码的offer方法的CAS思想,内有p.casNext方法,用了UNSAFE.compareAndSwapObject

并发容器总结

  • java.util.concurrent包提供的容器,分为3类:Concurrent*、CopyOnWrite*、Blocking*
  • Concurrent的特点是大部分通过CAS实现并发,而CopyOnWrite则是通过复制一份原数据来实现的,Blocking通过AQS实现的

原文链接:https://blog.csdn.net/LT11hka/article/details/117731740



所属网站分类: 技术文章 > 博客

作者:怎么没有鱼儿上钩呢

链接:http://www.javaheidong.com/blog/article/222113/4a96a27d255d8aceea11/

来源:java黑洞网

任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任

18 0
收藏该文
已收藏

评论内容:(最多支持255个字符)