我工作三年了,该懂并发了(干货)(三)

简介: 本文的组织形式如下,主要会介绍到同步容器类,操作系统的并发工具,Java 开发工具包(只是简单介绍一下,后面会有源码分析)。同步工具类有哪些。

避免锁:读-复制-更新

最快的锁是根本没有锁。问题在于没有锁的情况下,我们是否允许对共享数据结构的并发读写进行访问。答案当然是不可以。假设进程 A 正在对一个数字数组进行排序,而进程 B 正在计算其平均值,而此时你进行 A 的移动,会导致 B 会多次读到重复值,而某些值根本没有遇到过。

然而,在某些情况下,我们可以允许写操作来更新数据结构,即便还有其他的进程正在使用。窍门在于确保每个读操作要么读取旧的版本,要么读取新的版本,例如下面的树

微信图片_20220414221420.png

上面的树中,读操作从根部到叶子遍历整个树。加入一个新节点 X 后,为了实现这一操作,我们要让这个节点在树中可见之前使它"恰好正确":我们对节点 X 中的所有值进行初始化,包括它的子节点指针。然后通过原子写操作,使 X 称为 A 的子节点。所有的读操作都不会读到前后不一致的版本

微信图片_20220414221424.png

在上面的图中,我们接着移除 B 和 D。首先,将 A 的左子节点指针指向 C 。所有原本在 A 中的读操作将会后续读到节点 C ,而永远不会读到 B 和 D。也就是说,它们将只会读取到新版数据。同样,所有当前在 B 和 D 中的读操作将继续按照原始的数据结构指针并且读取旧版数据。所有操作均能正确运行,我们不需要锁住任何东西。而不需要锁住数据就能够移除 B 和 D 的主要原因就是 读-复制-更新(Ready-Copy-Update,RCU),将更新过程中的移除和再分配过程分离开。

Java 并发工具包

JDK 1.5 提供了许多种并发容器来改进同步容器的性能,同步容器将所有对容器状态的访问都串行化,以实现他们之间的线程安全性。这种方法的代价是严重降低了并发性能,当多个线程争抢容器锁的同时,严重降低吞吐量。

下面我们就来一起认识一下 Java 中都用了哪些并发工具

Java 并发工具综述

在 Java 5.0 中新增加了 ConcurrentHashMap 用来替代基于散列的 Map 容器;新增加了 CopyOnWriteArrayListCopyOnWriteArraySet  来分别替代 ArrayList 和 Set 接口实现类;还新增加了两种容器类型,分别是 QueueBlockingQueue, Queue 是队列的意思,它有一些实现分别是传统的先进先出队列 ConcurrentLinkedQueue以及并发优先级队列 PriorityQueue。Queue 是一个先入先出的队列,它的操作不会阻塞,如果队列为空那么获取元素的操作会返回空值。PriorityQueue 扩展了 Queue,增加了可阻塞的插入和获取等操作。如果队列为空,那么获取元素的操作将一直阻塞,直到队列中出现一个可用的元素为止。如果队列已满,那么插入操作则一直阻塞,直到队列中有可用的空间为止。

Java 6.0 还引入了 ConcurrentSkipListMapConcurrentSkipListSet 分别作为同步的 SortedMap 和 SortedSet 的并发替代品。下面我们就展开探讨了,设计不到底层源码,因为本篇文章主要目的就是为了描述一下有哪些东西以及用了哪些东西。

ConcurrentHashMap

我们先来看一下 ConcurrentHashMap 在并发集合中的位置

微信图片_20220414221428.png

可以看到,ConcurrentHashMap 继承了 AbstractMap 接口并实现了 ConcurrentMap 和 Serializable 接口,AbstractMap 和 ConcurrentMap 都是 Map 的实现类,只不过 AbstractMap 是抽象实现。

ConcurrentHashMap 和 Hashtable 的构造非常相似,只不过 Hashtable 容器在激烈竞争的场景中会表现出效率低下的现象,这是因为所有访问 Hashtable 的线程都想获取同一把锁,如果容器里面有多把锁,并且每一把锁都只用来锁定一段数据,那么当多个线程访问不同的数据段时,就不存在竞争关系。这就是 ConcurreentHashMap 采用的 分段锁 实现。在这种锁实现中,任意数量的读取线程可以并发的访问 Map,执行读取操作的线程和执行写入的线程可以并发的访问 Map,并且在读取的同时也可以并发修改 Map。

微信图片_20220414221431.png

ConcurrentHashMap 分段锁实现带来的结果是,在并发环境下可以实现更高的吞吐量,在单线程环境下只损失非常小的性能。

你知道 HashMap 是具有 fail-fast 机制的,也就是说它是一种强一致性的集合,在数据不一致的情况下会抛出 ConcurrentModificationException 异常,而 ConcurrentHashMap 是一种 弱一致性 的集合,在并发修改其内部结构时,它不会抛出 ConcurrentModificationException 异常,弱一致性能够容忍并发修改。

在 HashMap 中,我们一般使用的 size、empty、containsKey 等方法都是标准方法,其返回的结果是一定的,包含就是包含,不包含就是不包含,可以作为判断条件;而 ConcurrentHashMap 中的这些方法只是参考方法,它不是一个 精确值,像是 size、empty 这些方法在并发场景下用处很小,因为他们的返回值总是在不断变化,所以这些操作的需求就被弱化了。

image.gif微信图片_20220414221435.png

在 ConcurrentHashMap 中没有实现对 Map 加锁从而实现独占访问。在线程安全的 Map 实现 HashtableCollections.synchronizedMap 中都实现了独占访问,因此只能单个线程修改 Map 。ConcurrentHashMap 与这些 Map 容器相比,具有更多的优势和更少的劣势,只有当需要独占访问的需求时才会使用 Hashtable 或者是 Collections.synchronizedMap ,否则其他并发场景下,应该使用 ConcurrentHashMap。

ConcurrentMap

ConcurrentMap 是一个接口,它继承了 Map 接口并提供了 Map 接口中四个新的方法,这四个方法都是 原子性 方法,进一步扩展了 Map 的功能。

public interface ConcurrentMap<K, V> extends Map<K, V> {
  // 仅当 key 没有相应的映射值时才插入
  V putIfAbsent(K key, V value);
  // 仅当 key 被映射到 value 时才移除
  boolean remove(Object key, Object value);
  // 仅当 key 被映射到 value 时才移除
  V replace(K key, V value);
  // 仅当 key 被映射到 oldValue 时才替换为 newValue
  boolean replace(K key, V oldValue, V newValue);
}

ConcurrentNavigableMap

java.util.concurrent.ConcurrentNavigableMap 类是 java.util.NavigableMap 的子类,它支持并发访问,并且允许其视图的并发访问。

微信图片_20220414221440.png

什么是视图呢?视图就是集合中的一段数据序列,ConcurrentNavigableMap 中支持使用 headMapsubMaptailMap 返回的视图。与其重新解释一下 NavigableMap 中找到的所有方法,不如看一下 ConcurrentNavigableMap 中添加的方法

  • headMap 方法:headMap 方法返回一个严格小于给定键的视图
  • tailMap 方法:tailMap 方法返回包含大于或等于给定键的视图。
  • subMap 方法:subMap 方法返回给定两个参数的视图

ConcurrentNavigableMap 接口包含一些可能有用的其他方法

  • descendingKeySet()
  • descendingMap()
  • navigableKeySet()

更多关于方法的描述这里就不再赘述了,读者朋友们可自行查阅 javadoc

ConcurrentSkipListMap

ConcurrentSkipListMap 是线程安全的有序的哈希表,适用于高并发的场景。

微信图片_20220414221444.png

ConcurrentSkipListMap 的底层数据结构是基于跳表实现的。ConcurrentSkipListMap 可以提供 Comparable 内部排序或者是 Comparator 外部排序,具体取决于使用哪个构造函数。

ConcurrentSkipListSet

ConcurrentSkipListSet 是线程安全的有序的集合,适用于高并发的场景。ConcurrentSkipListSet 底层是通过 ConcurrentNavigableMap 来实现的,它是一个有序的线程安全的集合。

ConcurrentSkipListSet有序的,基于元素的自然排序或者通过比较器确定的顺序;

ConcurrentSkipListSet是线程安全的;

CopyOnWriteArrayList

CopyOnWriteArrayList 是 ArrayList 的变体,在 CopyOnWriteArrayList 中,所有可变操作比如 add、set 其实都是重新创建了一个副本,通过对数组进行复制而实现的。

微信图片_20220414221449.png

CopyOnWriteArrayList 其内部有一个指向数组的引用,数组是不会被修改的,每次并发修改 CopyOnWriteArrayList 都相当于重新创建副本,CopyOnWriteArrayList 是一种 fail-safe 机制的,它不会抛出 ConcurrentModificationException 异常,并且返回元素与迭代器创建时的元素相同。

每次并发写操作都会创建新的副本,这个过程存在一定的开销,所以,一般在规模很大时,读操作要远远多于写操作时,为了保证线程安全性,会使用 CopyOnWriteArrayList。

类似的,CopyOnWriteArraySet 的作用也相当于替代了 Set 接口。

微信图片_20220414221453.png


BlockingQueue

BlockingQueue 译为 阻塞队列,它是 JDK 1.5 添加的新的工具类,它继承于 Queue 队列,并扩展了 Queue 的功能。

微信图片_20220414221457.png

BlockingQueue 在检索元素时会等待队列变成非空,并在存储元素时会等待队列变为可用。BlockingQueue 的方法有四种实现形式,以不同的方式来处理。

  • 第一种是抛出异常
  • 特殊值:第二种是根据情况会返回 null 或者 false
  • 阻塞:第三种是无限期的阻塞当前线程直到操作变为可用后
  • 超时:第四种是给定一个最大的超时时间,超过后才会放弃

BlockingQueue 不允许添加 null 元素,在其实现类的方法 add、put 或者 offer 后时添加 null 会抛出空指针异常。BlockingQueue 会有容量限制。在任意时间内,它都会有一个 remainCapacity,超过该值之前,任意 put 元素都会阻塞。

BlockingQueue 一般用于实现生产者 - 消费者 队列,如下图所示

微信图片_20220414221502.png

BlockingQueue 有多种实现,下面我们一起来认识一下这些容器。

其中 LinkedBlockingQueueArrayBlockingQueue 是 FIFO 先入先出队列,二者分别和  LinkedListArrayList 对应,比同步 List 具有更好的并发性能。PriorityBlockingQueue 是一个优先级排序的阻塞队列,如果你希望按照某种顺序而不是 FIFO 处理元素时这个队列将非常有用。正如其他有序的容器一样,PriorityBlockingQueue 既可以按照自然顺序来比较元素,也可以使用 Comparator 比较器进行外部元素比较。SynchronousQueue 它维护的是一组线程而不是一组队列,实际上它不是一个队列,它的每个 insert 操作必须等待其他相关线程的 remove 方法后才能执行,反之亦然。

LinkedBlockingQueue

LinkedBlockingQueue 是一种 BlockingQueue 的实现。

微信图片_20220414221505.png

它是一种基于链表的构造、先入先出的有界阻塞队列。队列的 head 也就是头元素是在队列中等待时间最长的元素;队列的 tail也就是队尾元素是队列中等待时间最短的元素。新的元素会被插入到队尾中,检索操作将获取队列中的头部元素。链表队列通常比基于数组的队列具有更高的吞吐量,但是在大多数并发应用程序中,可预测的性能较差。

ArrayBlockingQueue

ArrayBlockingQueue 是一个用数组实现的有界队列,此队列顺序按照先入先出的原则对元素进行排序。

默认情况下不保证线程公平的访问队列,所谓公平访问队列指的是阻塞的线程,可以按照阻塞的先后顺序访问,即先阻塞线程先访问队列。非公平性是对先等待的线程是非公平的。有可能先阻塞的线程最后才访问队列。

PriorityBlockingQueue

PriorityBlockingQueue 是一个支持优先级的阻塞队列,默认情况下的元素采取自然顺序生序或者降序,也可以自己定义 Comparator 进行外部排序。但需要注意的是不能保证同优先级元素的顺序。

DelayQueue

DelayQueue 是一个支持延时获取元素的无阻塞队列,其中的元素只能在延迟到期后才能使用,DelayQueue 中的队列头是延迟最长时间的元素,如果没有延迟,则没有 head 头元素,poll 方法会返回 null。判断的依据就是 getDelay(TimeUnit.NANOSECONDS) 方法返回一个值小于或者等于 0 就会发生过期。

TransferQueue

TransferQueue 继承于 BlockingQueue,它是一个接口,一个 BlockingQueue 是一个生产者可能等待消费者接受元素,TransferQueue 则更进一步,生产者会一直阻塞直到所添加到队列的元素被某一个消费者所消费,新添加的transfer 方法用来实现这种约束。

TransferQueue 有下面这些方法:两个 tryTransfer 方法,一个是非阻塞的,另一个是带有 timeout 参数设置超时时间的。还有两个辅助方法 hasWaitingConsumergetWaitingConcusmerCount

LinkedTransferQueue

一个无界的基于链表的 TransferQueue。这个队列对任何给定的生产者进行 FIFO 排序,head 是队列中存在时间最长的元素。tail 是队列中存在时间最短的元素。

BlockingDeque

与 BlockingQueue 相对的还有 BlockingDeque 和 Deque,它们是 JDK1.6 被提出的,分别对 Queue 和 BlockingQueue 做了扩展。

微信图片_20220414221515.png

Deque 是一个双端队列,分别实现了在队列头和队列尾的插入。Deque 的实现有 ArrayDequeConcurrentLinkedDeque,BlockingDeque 的实现有 LinkedBlockingDeque

阻塞模式一般用于生产者 - 消费者队列,而双端队列适用于工作密取。在工作密取的设计中,每个消费者都有各自的双端队列,如果一个消费者完成了自己双端队列的任务,就会去其他双端队列的末尾进行消费。密取方式要比传统的生产者 - 消费者队列具有更高的可伸缩性,这是因为每个工作密取的工作者都有自己的双端队列,不存在竞争的情况。

ArrayDeque

ArrayDeque 是 Deque 的可动态调整大小的数组实现,其内部没有容量限制,他们会根据需要进行增长。ArrayDeque 不是线程安全的,如果没有外部加锁的情况下,不支持多线程访问。ArrayDeque 禁止空元素,这个类作为栈使用时要比 Stack 快,作为 queue 使用时要比 LinkedList 快。

除了 remove、removeFirstOccurrence、removeLastOccurrence、contains、interator.remove 外,大部分的 ArrayDeque 都以恒定的开销运行。

注意:ArrayDeque 是 fail-fast 的,如果创建了迭代器之后,却使用了迭代器外部的 remove 等修改方法,那么这个类将会抛出 ConcurrentModificationException 异常。

ConcurrentLinkedDeque

ConcurrentLinkedDeque 是 JDK1.7 引入的双向链表的无界并发队列。它与 ConcurrentLinkedQueue 的区别是 ConcurrentLinkedDeque 同时支持 FIFO 和 FILO 两种操作方式,即可以从队列的头和尾同时操作(插入/删除)。ConcurrentLinkedDeque 也支持 happen-before 原则。ConcurrentLinkedDeque 不允许空元素。

LinkedBlockingDeque

LinkedBlockingDeque 是一个由链表结构组成的双向阻塞队列,即可以从队列的两端插入和移除元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。LinkedBlockingDeque 把初始容量和构造函数绑定,这样能够有效过度拓展。初始容量如果没有指定,就取的是 Integer.MAX_VALUE,这也是 LinkedBlockingDeque 的默认构造函数。

相关文章
|
2月前
|
人工智能 架构师 项目管理
软件工程师,超过35岁怎么办
软件工程师,超过35岁怎么办
169 71
|
新零售 关系型数据库 数据库
从“如何设计用户超过1亿的应用”说起----数据库调优实战
作为SaaS服务提供商,数万乃至数十万级用户是业务架构设计上一开始就必须面对的问题。如何基于云服务平台设计并实施符合自身业务特点的系统架构,也是决定产品性能的关键。本文分享如何利用云服务,使用相对经济的方案,解决海量用户的数据库使用问题。
3619 0
|
6月前
|
消息中间件 Java 程序员
凭什么都是Java开发三年,而他能进大厂薪资是“我”2倍?
刚毕业的前三年,你会觉得自己是在学习,于是无牵无挂。但三年以后,如果年龄和能力不匹配,你能进入 BAT、TMD 这样的大厂的机会实在渺茫。
|
算法 前端开发 Java
面试机会增加100倍,建议收藏!
面试机会增加100倍,建议收藏!
117 0
涨姿势了!原来这才是多线程正确实现方式
线程同步机制是一套适用于协调线程之间的数据访问机制,该机制可以保障线程安全 java平台提供的线程同步机制包括:锁、volatile关键字、final关键字,static关键字、以及相关API如object.wait/object.notify
|
缓存 iOS开发 芯片
CPU多核增20%符预期,GPU大涨50%达3万分,苹果M2跑分曝光
CPU多核增20%符预期,GPU大涨50%达3万分,苹果M2跑分曝光
201 0
基本时间单位 | 带你读《5G 空口设计与实践进阶 》之十五
为提供精确、一致的时间度量,NR 定义了最小时间单位 Tc。
基本时间单位 | 带你读《5G 空口设计与实践进阶 》之十五
|
缓存 监控 前端开发
项目维护几年了,为啥还这么卡?
前段时间有个客户问我,为啥你们项目都搞了好几年了,为啥线上还会经常反馈卡顿,呃呃呃。。
|
安全 Java 容器
我工作三年了,该懂并发了(干货)(一)
本文的组织形式如下,主要会介绍到同步容器类,操作系统的并发工具,Java 开发工具包(只是简单介绍一下,后面会有源码分析)。同步工具类有哪些。
我工作三年了,该懂并发了(干货)(一)
|
Java 编译器 程序员
我工作三年了,该懂并发了(干货)(二)
本文的组织形式如下,主要会介绍到同步容器类,操作系统的并发工具,Java 开发工具包(只是简单介绍一下,后面会有源码分析)。同步工具类有哪些。
我工作三年了,该懂并发了(干货)(二)