重读Java并发编程艺术(5) - 并发容器和框架

1. ConcurrentHashMap

1.1 优点

  • HashMap代替线程不安全
  • HashTable效率低下(synchronized)
  • ConcurrentHashMap锁分段技术可有效提升并发访问率

1.2 结构

  • ConcurrentHashMap
    • Segment (分段锁,可重入锁,类似HashMap的数组和链表结构)
      • HashEntry(链表结构元素)

一个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阻塞队列

  1. ArrayBlockingQueue: 一个由数组结构组成的有界阻塞队列;
  2. LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列;
  3. PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列;
  4. DelayQueue: 一个使用优先级队列实现的无界阻塞队列;
  5. SynchronousQueue: 一个不存储元素的阻塞队列;
  6. LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列;
  7. LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。
2.3.4.1 ArrayBlockingQueue

有界,默认非公平(保证公平性会降低吞吐量),其使用可重入锁实现公平性:

public 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值