一、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)操作,如果这时候有一个线程插队,尾节点可能变化,则需要暂停入队,重新获取新的尾节点。
实现
- 方法:循环+CAS
- 定位出尾节点(期间可能有其他线程插入新的节点)
- 定位成功后,通过CAS将入队的节点设置成尾节点的next节点
- 如果尾节点后面有多余HOPS个节点,通过CAS更新tail节点为当前的入队节点
- 使用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并发编程的艺术