Java并发容器与框架的实现

一、ConcurrentHashMap

1.1 背景

多线程中,HashMap导致程序死循环(HashMap采用拉链法解决hash冲突,当链表大于了装载因子对应的最大容量,需要重新进行散列。问题发生在这里,多线程可能同时出发rehash,形成了链表的回路);HashTable会阻塞其他线程,效率低下。ConccurentHashMap使用分段锁,把数据分段,每一段配一把锁。

1.2 结构

由Segment数组和HashEntry数组组成。Segment是一个ReentrantLock,当作锁。HashEntry用于存储键值对数据,是一个链表的结构。一个CHM里面有一个Segment数组,一个Segment里面有一个HashEntry数组。

1.3 操作

1.3.1 初始化:sengment数组、段偏移segmentShift、段掩码segmentMask

  • 根据concurrencyLevel计算出segment数组长度ssize(一个大于等于concurrencyLevel的最小2的N次方的值)
  • segmentShift等于32-N(1左移多少位等于ssize)
  • segmentMask是ssize-1
  • 默认的concurrencyLevel = 16,ssize=65536,segmentShift=32-4=28,segmentMask=65535

1.3.2 get操作:线程安全的,因为value和count字段都是volatile类型的

先对hashcode经过一次再散列(目的是减少冲突),然后使用散列值运算定位到Segment,再通过散列算法定位到具体的元素。

  • 定位Segment使用的hash算法 hash >>> segmentShift & segmentMask
  • 定位HashEntry使用的hash算法 int index = hash & (tab.length - 1)

1.3.3 put操作:需要对共享变量加锁

  • 先定位到Segment,判断是否需要对Segment里main的HashEntry扩容(判断当前Segment里面的HashEntry数组是否超过阈值,超过的话,创建一个双倍容量的数组,把原来数组的元素再散列后装入新的数组),然后再定位需要添加到HashEntry的元素的位置。

1.3.4 size操作:统计整个CHM的元素大小

  • 直接把所有segment的count相加,尝试加两次。比较前后两次的modCount是否变化(put、remove、clean操作前都会将++modCount)

二、ConcurrentLinkedQueue(非阻塞的线程安全队列、使用CAS算法)

  • 由head、tail两个节点组成,每个节点有next域,链队结构
  • 初始化情况下,head存储元素为空,tail=head
  • 尾节点:队列的最后一个节点,不一定等于tail节点

2.1 入队

示意图:
在这里插入图片描述

入队过程

1) 入队节点设置成当前尾节点的下一个节点
2) 如果tail的next不为null,入队的节点设置为tail。(体现出尾节点和tail的区别)

多线程入队面临的问题

当一个线程正在入队,需要获取尾节点,然后进行(1)操作,如果这时候有一个线程插队,尾节点可能变化,则需要暂停入队,重新获取新的尾节点。

实现
  1. 方法:循环+CAS
  2. 定位出尾节点(期间可能有其他线程插入新的节点)
  3. 定位成功后,通过CAS将入队的节点设置成尾节点的next节点
  4. 如果尾节点后面有多余HOPS个节点,通过CAS更新tail节点为当前的入队节点
  5. 使用HOPS的原因:减少CAS更新tail的次数,只有当节点和尾节点距离大于HOPS才更新tail为尾节点。(HOPS默认为1)
    在这里插入图片描述

出队过程

在这里插入图片描述

当head有元素时,直接弹出head里的元素,当head没有元素,出队操作会更新head节点。
实现
  • 循环+CAS
  • 首先获取头节点元素,判断是否为空
  • 如果为空,表示另一个线程已经取走
  • 如果不为空,使用CAS将头节点的引用设置成null,如果CAS失败,表示另一个线程进行出队操作,更新了head节点,需要重新获取头节点,如果成功,返回头节点元素的值。

三、阻塞队列

概念:支持阻塞的插入和移除方法的队列,通常用于生产者-消费者场景

Java中插入和移除的4种处理方式

方法抛出异常返回值阻塞超时退出
插入add(e)offer(e)put(e)offer(e, time, unit)
移除remove()poll()take()poll(time, unit)
查看element()peek()--

队列满,往里插入元素,抛出IllegalStateException
队列空,取元素,抛出NoSuchElementException

Java中的阻塞队列

  • ArrayBlockingQueue 有界,数组实现
  • LinkedBlockingQueue 有界,链表实现
  • PriorityBlockingQueue 无界,支持优先级
  • DelayQueue 无界支持延时获取元素,优先队列实现
  • SynchronousQueue 不存储元素,用于传递数据。
  • LinkedTransferQueue 无界,链表实现
  • LinkedBlockingDeque 双向阻塞队列,链表实现

四、Fork/Join框架

工作窃取算法

  • 前提:大任务被分成互不依赖的子任务,每个子任务放到各自的队列里,每个队列创建一个单独的线程执行。
  • 概念:指的是某个线程从其他线程的队列窃取任务进行执行
  • 改进:减少任务窃取线程和被窃取任务的线程对任务的竞争,通常使用双端队列。被窃取任务的线程从双端队列头部拿任务,窃取任务的线程从队列尾部拿任务执行。
  • 原理:任务分割出来的子任务会从当前工作线程的双端队列头部进入,当一个工作线程的队列没有任务了,随机从其他工作线程的队列的尾部窃取一个任务来执行。
    在这里插入图片描述

使用Fork/Join

  • ForkJoinTask提供执行fork和join的操作机制。通常使用集成他们的子类RecursiveAction(无返回结果的任务)或者RecursiveTask(有返回结果的任务),重写compute方法来使用。
  • ForkJoinPool 用来执行ForkJoinTask
    在这里插入图片描述
    在这里插入图片描述
    参考资料

【1】Java并发编程的艺术

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值