java 并发容器 笔记

java并发容器

知识准备:Hash,位运算
一般hash算法:(md5,sha-1)摘要算法,取模,取整…
hash冲突解决方法:

  1. 开放寻址
  2. 再散列
  3. 链地址法

位运算与取模:取模a%(2^n) ==a&(2 ^n-1),所以map中的数组个数一定是2的n次方长度,计算keyhash值时,用位运算定位更快
在这里插入图片描述
1.7 HashMap 多线程put下死循环分析:

  1. resize过程中,旧链表移动到新链表过程,使用的头插法:加到链表的头位置
  2. 由于resize过程中transfer的线程挂起和执行先后顺序的问题,产生循环链表
  3. 后续再get的时候如果key落入了该链表对应的桶, 然后该key对应的value不存在的情况下, 就会在循环链表上死循环寻找

1.7&1.8:比较
1.7:

  1. 分段锁Segment…继承了ReentrantLock…可重入锁,KV元素是保存在Segment中的HashEntry的数组,再形成链表
  2. get方法不需要加锁是因为HashEntry中的value和 next是用 volatile 来修饰的,所以可以拿到最新的
  3. get方法获取值时,用hash值的高位来定位所属Segment, 用hash值的全部来定位Segment中hashEntry数组的下标
  4. put方法通过CAS方式来确保只有一个线程可以初始化成功
  5. put方法通过CAS方式trylock(),cas重试次数为(cpu核数>1?64:1次),如果还拿不到锁,就会直接通过阻塞方式lock()拿锁
  6. put方法拿到锁之后,采用头插法将元素插入链表
  7. size方法是通过CAS方式统计Segment的modCount,自旋两次,如果两次的统计数相同则返回,不相同则通过遍历所有Segment的lock()来加锁进行统计,会锁住所有Segment,不推荐,在多线程下统计size也不太准确.同样不推荐的还有containsValue()方法,也会锁住所有.推荐使用isEmpty()来判断是否为空
  8. 是弱一致的,因为get没加锁,遍历时可能数据已过期,要想用强一致的,请使用HashTable,或使用Collections.synchronizedMap方法包装普通Map
  9. 并发度:Segment数,默认16,如果手动设置的话, 会取16的2N次方数中离设置数最近的那个数来设置

在这里插入图片描述
1.8:

  1. 取消了Segment,直接改为数组+(红黑树少于6)链表/(链表超过8)红黑树的方式来实现
  2. HashEntry改成Node(继承自Entry)了,几乎一模一样
  3. 转变为红黑树时使用的TreeNode继承自Node,区别于HashMap转变为红黑树的TreeNode是继承自LinkedHashMap.Entry
  4. 转为红黑树时,树根节点为TreeBin(即保存在数组中的那个元素), 使用cas+synchronized方式来保证线程安全,提供一些锁属性,区别于1.7的加lock阻塞方式
  5. 当需要扩容时,会用ForwardingNode来替换原数组的Node,表示该节点被移动,涉及到并发扩容操作
  6. volatile int sizeCtl属性, 如果为-1表示正在初始化, 如果为 -n表示正在有N个线程正在执行扩容,如果为0(默认值)表示线程还未初始化,如果为正数表示数组扩容的阈值,为当前容量的1.75倍
  7. tabAt, 通过硬件层面的原子操作获取volatile value,所以保证每次获取数据都是最新的
    casTabAt 通过cas操作来设置数据
    setTabAt 通过硬件层面的cas操作来设置数据
  8. ConcurrentHashMap 被创建出来时,没有做初始化操作,真正初始化时,是在第一次put方法时进行(创建数组table)
  9. get方法 1) 如果元素落在table数组里, 直接返回 2)如果落在红黑树里,调用 find方法在树里找, 3)如果在链表中,遍历链表
  10. put时,使用死循环进行插入数据
    1. cas方式初始化table数组
    2. 计算扩容阈值
    3. cas方式替换数据
    4. 如果table正在扩容,当前线程加入帮助扩容处理
    5. 使用synchronized锁来锁住table中hash对应下标的元素,然后向后插入数据(尾插法)
    6. 链表满足8长度,转为红黑树
    7. 检查是否需要扩容,并操作之,1.75倍 ,扩容后的table长度是2的倍数
      ps:HashMap 2倍扩容, HashTable 2n+1扩容
  11. remove时,使用synchronized锁来锁住hash对应的table下标元素,遍历链表或树,删除node,检查是否需要红黑树转为链表
  12. 扩容时可能是多线程扩容,采用**步长(由Cpu数动态计算的)**的方式来把扩容工作拆分到多线程上,例如:线程1处理下标0,5,10,线程2处理1,6,11 线程3处理2,7,12…
  13. size拿集合个数时,直接返回(baseCount+CountCell(当cas累加baseCount失败时,累加到此值)数组统计),这个数在每次put时cas直接统计,区别于1.7的锁全部Segment
  14. remove时,扣减数量,直接从countCell中扣减,不去争夺baseCount
  15. 为什么使用synchronized来替换reentrantLock? 在1.8中对synchronized关键字的操作做了大量优化,作为语言及关键字,开销比reentrantLock更小
  16. 并发度:在1.8中没有什么限制…
    在这里插入图片描述
    ==================================
    ConcurrentSkipListMap 和 ConcurrentSkipListSet 介绍

1.用来存储有序的Map和有序的Set
2. ConcurrentSkipListSet 内部是包装了ConcurrentSkipListMap
3. 数据结构:跳表,先大步快进确定范围,再在小范围中查找目标,会跳过一些不需要检查的元素,插入节点时,会随机判断是否需要将新加的元素设置为索引节点
4. 是概率数据结构的一种,redis,lucene等都使用了此算法
在这里插入图片描述
ConcurrentLinkedQueue无界非阻塞队列 相当于 LinkedList的并发版本
…add队头插入/offer队尾插入/peek拿队头不移除/poll拿队头并移除

CopyOnWriteArrayList 和 CopyOnWriteArrayList 写时复制容器
1.在写入时,会把当前数据复制创建新副本,并把数据写入到新副本,然后把容器指向新副本
2.读写分离,写的同时读的是旧副本,所以不需要加读锁,适用于读多写少场景
3.修改操作频繁复制,内存开销大,性能低
4.只能保证最终一致性,因为读取时总是读取旧版本数据
5.最好批量提交数据,减少复制次数

阻塞队列
在这里插入图片描述
有界:长度有限,不会扩容在这里插入图片描述
ArrayBlockingQueue 读写公用一把锁,必须指定容量大小
LinkedBlockingQueue, 读写锁分离,不互斥,默认容量为Integer最大值,实际还应该手动指定大小
PriorityBlockingQueue,用堆实现的
DelayQueue内部包装了PriorityBlockingQueue,延时获取元素,只有到期的元素才能take到,可用于订单过期通知,缓存过期通知
SynchronousQueue put线程会阻塞,等待take线程拿走元素后,put线程才会释放
LinkedTransferQueue 当前有消费者正在等待消息时,生产者会直接把元素传递给消费者,跳过元素放入队列再出队列的操作;当生产者调用transfer方法时,会阻塞,直到有消费者取完元素时,才会释放
LinkedBlockingQueue 双向阻塞,从队列头/尾皆可存取数据

在JDK中有专门的接口:BlockingQueue
1.当队列满,阻塞的插入
2.当队列空,阻塞的移除
3.阻塞方法成对出现(add/remove)(offer/poll)(put/take 这两个是接口定义的阻塞式方法,具体子类实现可能不遵循此定义) ,工作密取(forkjoin中)模式会用到此队列

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值