文章目录
- 1. ConcurrentHashMap
- 2. 线程安全队列
-
- 2.1 线程安全队列实现方式:
- 2.2 ConcurrentLinkedQueue
- 2.3 阻塞队列 BlockingQueue
- 3. Fork/Join 框架
1. ConcurrentHashMap
1.1 优点
- HashMap代替线程不安全
- HashTable效率低下(synchronized)
- ConcurrentHashMap锁分段技术可有效提升并发访问率
1.2 结构
- ConcurrentHashMap
- Segment (分段锁,可重入锁,类似HashMap的数组和链表结构)
- HashEntry(链表结构元素)
- Segment (分段锁,可重入锁,类似HashMap的数组和链表结构)
一个ConcurrentHashMap中包含一个Segment数组,每个Segment数组包含一个HashEntry数组,每个HashEntry是一个链表结构的元素。
当对HashEntry数组的数据进行修改时,必须首先获得与它对应的Segment锁。
1.3 操作
1.3.1 get操作
- 先经过一次再散列,用这个散列值通过散列运算定位到Segment,再通过散列算法定位到元素。
- get操作不需要加锁,通过将共享变量(如统计当前Segment大小的count字段和HashEntry的value)定义成volatile类型实现。支持多线程读和单线程写(写入值不依赖原值时支持多线程写)。
1.3.2 put操作
1.3.2.1 过程
- 需要加锁,先定位到Segment,在Segment中进行插入操作。
- 插入操作包括两个步骤:
- 判断 Segment 中 HashEntry 是否需要扩容;
- 定位添加元素的位置并添加到HashEntry数组中。
1.3.2.2 扩容
- 通过HashEntry 数组大小是否超过阈值判断是否扩容。Segment 扩容比 HashMap 更恰当,HashMap 是插入后判断的,有可能扩容后不再有新元素插入,这时HashMap就进行了一次无效的扩容。
- 扩容过程:创建一个两倍原来容量的数组,将原数组元素再散列后插入新的数组里。
- ConcurrentHashMap 不会对整个容器进行扩容,而只对某个Segment进行扩容。
1.3.3 size操作
先尝试2次不加锁方式统计各个Segment大小之和,如果统计过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment大小之和。
判断统计的时候容器是否发生变化:在put、remove 和 clean 方法里操作元素前都会将变量 modCount 进行加1, 故可以统计size前后比较 modCount 是否发生变化。
2. 线程安全队列
2.1 线程安全队列实现方式:
- 阻塞方式:一个锁(入队出队用同一把锁)或两个锁(入队出队用不同的锁)
- 非阻塞方式:循环CAS实现
2.2 ConcurrentLinkedQueue
2.2.1 特点
非阻塞,基于链接节点的无界线程安全队列,采用先进先出规则对节点排序,添加元素时加到队列尾部,取出元素时返回队列头部元素。采用"wait-free"算法(即CAS)。
2.2.2 结构
- ConcurrentLinkedQueue 由 head 节点和 tail 节点组成;
- 每个节点(Node)由节点元素 item 和指向下一个节点的应用 next 组成;
- 默认情况 head 节点存储的元素为空,tail 节点等于 head 节点。
2.2.3 入队
2.2.3.1 入队过程
将入队节点添加到队列尾部。
- 将入队节点设置成当前队列尾节点的下一个节点;
- 更新 tail 节点:
- 如果 tail 节点的 next 节点为空,设置 tail 节点的 next 为入队节点;
- 如果tail节点的next节点不为空,设置 tail 节点为入队节点。
(所以 tail 节点不总是尾节点)
2.2.3.2 HOPS设计意图
为什么不让 tail 永远指向队列尾节点?
- 为了避免每次都使用循环CAS更新 tail 节点,提高效率。
- 当 tail 节点和尾节点的距离大于等于常量 HOPS(默认为1)时才更新 tail 节点。
(用定位尾节点操作代替每次更新 tail 节点,本质上是通过增加对 volatile 变量的读操作来减少对 volatile 变量的写操作)
注意:入队方法永远返回 true,故不要通过返回值判断入队是否成功。
2.2.4 出队
不是每次出队都更新 head 节点,
- 当 head 节点里有元素时,直接弹出 head 节点里的元素;
- 当 head 节点里没有元素时,出队操作才会更新 head 节点。
(同样是通过 hops 变量减少使用 CAS 更新 head 节点的消耗)
2.3 阻塞队列 BlockingQueue
2.3.1 概念
一个支持两个附加操作的队列:
- 支持阻塞的插入方法:当队列满时,队列阻塞插入元素的线程,直到队列不满。
- 支持阻塞的移除方法:队列为空时,获取元素的线程会等待队列变为非空。
2.3.2 应用场景
- 常用于生产者和消费者的场景
2.3.3 插入移除操作的4种处理方式
方法/处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除方法 | remove() | poll() | take() | poll(time, unit) |
检查方法 | element() | peek() | 不可用 | 不可用 |
- 抛出异常:
- 队列满时,插入元素抛出 IllegalStateException(“Queue full”) 异常;
- 队列空时,取出元素抛出 NoSuchElementException 异常;
- 返回特殊值:
- 插入元素时,返回元素是否插入成功,成功返回true;
- 移除元素时,从队列里取出一个元素,没有则返回null;
- 一直阻塞:
- 队列满时,生产者线程 put 元素,队列会一直阻塞生产者线程,直到队列可用或响应中断退出。
- 队列空时,消费者线程 take 元素,队列会阻塞消费者线程,直到队列不为空;
- 超时退出:
- 当队列满时,生产者线程插入元素会阻塞生产者线程一段时间,超过指定时间,生产者线程就退出;
- 当队列空时,消费者线程取出元素会阻塞消费者线程一段时间,超过指定时间,返回null。
若是无界阻塞队列则不可能出现队列满的情况,所以使用put或offer方法永远不会被阻塞,且offer方法永远返回true。
2.3.4 Java阻塞队列
- ArrayBlockingQueue: 一个由数组结构组成的有界阻塞队列;
- LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列;
- PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列;
- DelayQueue: 一个使用优先级队列实现的无界阻塞队列;
- SynchronousQueue: 一个不存储元素的阻塞队列;
- LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列;
- LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。
2.3.4.1 ArrayBlockingQueue
有界,默认非公平(保证公平性会降低吞吐量),其使用可重入锁实现公平性:
public