Java进阶篇--并发容器之ConcurrentLinkedQueue

2023-10-21 03:28

本文主要是介绍Java进阶篇--并发容器之ConcurrentLinkedQueue,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

ConcurrentLinkedQueue简介

Node

操作Node的几个CAS操作

offer方法

poll方法

HOPS的设计

扩展知识


ConcurrentLinkedQueue简介

在单线程编程中常用的集合类,如ArrayList和HashMap等,但是这些类都不是线程安全的类。为了保证线程安全,可以使用Vector作为替代,但其通过使用synchronized独占锁在方法级别实现线程安全,从而使多线程执行变为串行化,效率较低。另外,也可以使用Collections.synchronizedList(List<T> list)将ArrayList转换为线程安全的,但仍然是通过synchronized修饰方法实现的,依然不是高效的方式。

针对队列这种常用的数据结构,在解决线程安全问题时,Doug Lea大师提供了ConcurrentLinkedQueue,这是一个线程安全的队列。根据类名可以看出,它基于链表实现队列的数据结构。

Node

Node节点是用于链表数据结构中,构成ConcurrentLinkedQueue的基本单元。节点包含了两个字段:一个是item,用于存储元素数据;另一个是next,用于指向下一个节点,从而实现链表结构。

在ConcurrentLinkedQueue中,由于要支持并发操作,因此使用了volatile关键字对节点的item和next字段进行修饰。volatile关键字可以保证在多线程环境下的可见性,从而避免了出现脏读等线程安全问题。

在ConcurrentLinkedQueue的构造函数中,会初始化头尾节点,同时将head和tail指向同一个初始节点。这个节点的item为空(null),表示队列中还没有任何元素。

通过不断添加和删除节点,就可以实现ConcurrentLinkedQueue,该队列是线程安全的,由于使用了无锁算法(CAS操作),因此具有较高的吞吐量。

操作Node的几个CAS操作

在ConcurrentLinkedQueue中,对Node节点的CAS操作(关于CAS操作可以看这篇文章),有以下几个方法:

  • casItem(E cmp, E val): 用于比较并交换节点的数据域item。这个操作会比较节点的item域是否等于cmp,如果相等则将其替换为val。返回true表示交换成功,返回false表示交换失败。
  • lazySetNext(Node<E> val): 用于延迟设置节点的下一个节点指针next。这个操作会将节点的next字段设置为val,但不保证立即可见。即使在执行这个操作之后,其他线程读取到的节点的next值可能仍然是旧值。这种操作通常在性能上比强制可见性要好。
  • casNext(Node<E> cmp, Node<E> val): 用于比较并交换节点的下一个节点指针next。这个操作会比较节点的next域是否等于cmp,如果相等则将其替换为val。返回true表示交换成功,返回false表示交换失败。
//更改Node中的数据域item	
boolean casItem(E cmp, E val) {return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
}
//更改Node中的指针域next
void lazySetNext(Node<E> val) {UNSAFE.putOrderedObject(this, nextOffset, val);
}
//更改Node中的指针域next
boolean casNext(Node<E> cmp, Node<E> val) {return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}

需要注意的是,这些方法中都是通过调用sun.misc.Unsafe类的相关方法来实现CAS操作。Unsafe类是Java底层提供的一个类,用于支持直接操作内存和并发操作,它提供了一些原子性操作的方法,包括compareAndSwapObject()等方法。这些方法利用处理器指令集的CMPXCHG指令来实现原子性的比较和交换操作。

CAS操作在并发编程中非常重要,它可以避免使用锁机制而实现线程安全,提高并发性能。然而,需要注意的是CAS操作并不是适用于所有情况,有时候可能会存在ABA问题等需要注意的情况。

offer方法

1. 单个线程offer:当只有一个线程执行offer操作时,它会将元素插入到队列的末尾。由于没有其他线程执行poll操作,所以队列的长度会不断增长。

2. 多个线程offer:当多个线程同时执行offer操作时,它们会将元素插入到队列的末尾。因为offer操作总是在队列的末尾进行,而poll操作总是在队列的队头进行,所以这两类线程并不会相互影响。

3. 部分线程offer,部分线程poll:

  • offer速度快于poll:如果offer操作的速度比poll操作快,那么队列的长度会越来越长。因为offer节点总是在队列的末尾插入,而poll节点总是在队列的队头删除,所以两类线程之间没有交集,可以看作是一个"单线程offer"的情况。
  • offer速度慢于poll:如果poll操作的速度比offer操作快,那么队列的长度会越来越短。此时,offer线程和poll线程会存在交集,即在某一时刻会发生同时操作的节点,这个时刻被称为临界点。在临界点时,需要考虑在offer方法中的处理逻辑。

在多线程环境下,ConcurrentLinkedQueue的offer方法可以保证并发安全,无需额外的同步措施。它能够高效地支持并发插入操作,并且遵循FIFO(先进先出)特性。

import java.util.concurrent.ConcurrentLinkedQueue;public class main {public static void main(String[] args) {// 创建一个ConcurrentLinkedQueue对象ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();// 单个线程offerThread singleThreadOffer = new Thread(() -> {for (int i = 0; i < 5; i++) {queue.offer(i);System.out.println("单个线程offer元素:" + i);}});// 多个线程offerThread multipleThreadsOffer = new Thread(() -> {for (int i = 5; i < 10; i++) {queue.offer(i);System.out.println("多个线程offer元素:" + i);}});// 部分线程offer,部分线程pollThread mixedThreads = new Thread(() -> {for (int i = 0; i < 5; i++) {if (i % 2 == 0) {queue.offer(i);System.out.println("部分线程offer元素:" + i);} else {Integer element = queue.poll();System.out.println("部分线程poll元素:" + element);}}});// 启动线程singleThreadOffer.start();multipleThreadsOffer.start();mixedThreads.start();// 等待线程执行完毕try {singleThreadOffer.join();multipleThreadsOffer.join();mixedThreads.join();} catch (InterruptedException e) {e.printStackTrace();}}
}

上述代码演示了三种场景:

  • 单个线程执行offer操作,将元素逐个插入队列;
  • 多个线程同时执行offer操作,将元素逐个插入队列;
  • 部分线程执行offer操作,部分线程执行poll操作,实现元素的插入和移除。

在每个线程中,通过queue.offer(i)方法将元素插入队列,使用queue.poll()方法从队列移除元素。每个操作都会打印出相应的信息以便观察结果。

ConcurrentLinkedQueue的offer和poll方法可以安全地在多线程环境下使用,并且遵循FIFO(先进先出)特性。

poll方法

  • 在单线程下,poll操作会先判断队头节点的数据是否为空,如果不为空,则直接返回数据并将该节点出队;否则需要遍历队列寻找数据不为空的节点,并更新队头的指向。
  • 在多线程情况下,需要注意处理多个线程同时poll和offer的情况,保证操作的正确性。同时,在判断队列是否为空的时候,不能仅仅依靠poll返回值是否为null,而应该使用isEmpty方法进行判断。
import java.util.Queue;
import java.util.concurrent.*;public class main {// 创建一个ConcurrentLinkedQueue对象private static Queue<Integer> queue = new ConcurrentLinkedQueue<>();public static void main(String[] args) throws InterruptedException {// 将数字放入队列中queue.offer(1);queue.offer(2);queue.offer(3);// 从队列中取出元素并输出Integer value = queue.poll();System.out.println("取出的值为:" + value);System.out.println("当前队列中的元素为:" + queue.toString());// 创建一个线程池ExecutorService executor = Executors.newCachedThreadPool();// 循环10次,每次提交一个任务到线程池中for (int i = 0; i < 10; i++) {executor.execute(() -> {// 将数字放入队列中queue.offer((int) (Math.random() * 100));// 从队列中取出元素并输出Integer result = queue.poll();System.out.println(Thread.currentThread().getName() + " 取出的值为:" + result);System.out.println("当前队列中的元素为:" + queue.toString());});}// 关闭线程池executor.shutdown();// 等待线程池中的任务执行完成executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);}
}

在这个综合代码示例中,首先在单个线程环境下将数字放入队列中并取出元素,并输出队列的当前状态。接着,在多线程环境下,我们创建了一个线程池并提交了10个任务,每个任务都会随机生成一个数字放入队列中,然后再从队列中取出元素并输出,最后输出队列的当前状态。可以看到,在单个线程和多个线程的情况下,使用ConcurrentLinkedQueue的poll方法都能够保证队列操作的线程安全性,并且在多线程环境下能够提高程序的并发性能。

HOPS的设计

通过上面对offer和poll方法的分析,我们发现tail和head是延迟更新的,两者更新触发时机为:

HOPS的设计通过延迟更新的方式,减少CAS操作的频率,从而提升入队操作的性能。具体来说,tail的更新被延迟至插入节点之后,只有当tail指向的节点的下一个节点不为null时才会进行真正的队尾节点定位和更新操作。同样地,head的更新也被延迟至删除节点之后,只有当head指向的节点的item域为null时才会进行队头节点定位和更新操作。

这种延迟更新策略的设计意图是为了减少CAS操作对性能的影响。如果每次都立即更新tail或head,那么大量的入队或出队操作都需要执行CAS操作来更新tail或head,从而对性能产生较大的负担。通过延迟更新,在一定程度上减少了CAS操作的频率,提升了入队操作的效率。

尽管延迟更新会增加在循环中定位队尾节点的操作,但整体而言,读取操作的性能要远远高于写操作。因此,增加的在循环中定位尾节点的操作性能损耗相对较小。

总结来说,HOPS设计中延迟更新的策略通过减少CAS操作的频率,提升了入队操作的性能,同时在读取操作性能与写入操作性能的权衡中取得了较好的平衡。

扩展知识

tail和head是ConcurrentLinkedQueue队列中两个重要的指针。它们分别指向队列中的尾节点和头节点。

当需要往队列中插入元素时,我们需要使用tail指针指向的节点作为插入节点的前驱节点,并通过CAS(compare-and-swap)操作将插入节点加在其后面。如果插入节点成功地加入了队列尾部,则需要使用CAS操作将tail指针指向新的队尾节点。

当需要从队列中删除元素时,我们需要使用head指针指向的节点作为要删除的节点,并通过CAS操作将其指向下一个节点。如果删除成功,则需要使用updateHead方法将head指针指向真正的队头节点。

需要注意的是,tail和head指针的更新会存在延迟,只有在特定条件下才会进行更新。具体来说,当tail指向的节点的下一个节点不为null时,会执行定位队列真正的队尾节点的操作,并通过CAS操作完成tail的更新;当head指向的节点的item域为null时,会执行定位队列真正的队头节点的操作,并通过updateHead方法完成head的更新。这种延迟更新策略可以有效地减少CAS操作的频率,提高队列的性能。

这篇关于Java进阶篇--并发容器之ConcurrentLinkedQueue的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/251598

相关文章

Spring Boot项目部署命令java -jar的各种参数及作用详解

《SpringBoot项目部署命令java-jar的各种参数及作用详解》:本文主要介绍SpringBoot项目部署命令java-jar的各种参数及作用的相关资料,包括设置内存大小、垃圾回收... 目录前言一、基础命令结构二、常见的 Java 命令参数1. 设置内存大小2. 配置垃圾回收器3. 配置线程栈大小

SpringBoot实现微信小程序支付功能

《SpringBoot实现微信小程序支付功能》小程序支付功能已成为众多应用的核心需求之一,本文主要介绍了SpringBoot实现微信小程序支付功能,文中通过示例代码介绍的非常详细,对大家的学习或者工作... 目录一、引言二、准备工作(一)微信支付商户平台配置(二)Spring Boot项目搭建(三)配置文件

如何高效移除C++关联容器中的元素

《如何高效移除C++关联容器中的元素》关联容器和顺序容器有着很大不同,关联容器中的元素是按照关键字来保存和访问的,而顺序容器中的元素是按它们在容器中的位置来顺序保存和访问的,本文介绍了如何高效移除C+... 目录一、简介二、移除给定位置的元素三、移除与特定键值等价的元素四、移除满足特android定条件的元

解决SpringBoot启动报错:Failed to load property source from location 'classpath:/application.yml'

《解决SpringBoot启动报错:Failedtoloadpropertysourcefromlocationclasspath:/application.yml问题》这篇文章主要介绍... 目录在启动SpringBoot项目时报如下错误原因可能是1.yml中语法错误2.yml文件格式是GBK总结在启动S

Spring中配置ContextLoaderListener方式

《Spring中配置ContextLoaderListener方式》:本文主要介绍Spring中配置ContextLoaderListener方式,具有很好的参考价值,希望对大家有所帮助,如有错误... 目录Spring中配置ContextLoaderLishttp://www.chinasem.cntene

java实现延迟/超时/定时问题

《java实现延迟/超时/定时问题》:本文主要介绍java实现延迟/超时/定时问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Java实现延迟/超时/定时java 每间隔5秒执行一次,一共执行5次然后结束scheduleAtFixedRate 和 schedu

Java Optional避免空指针异常的实现

《JavaOptional避免空指针异常的实现》空指针异常一直是困扰开发者的常见问题之一,本文主要介绍了JavaOptional避免空指针异常的实现,帮助开发者编写更健壮、可读性更高的代码,减少因... 目录一、Optional 概述二、Optional 的创建三、Optional 的常用方法四、Optio

Spring Boot项目中结合MyBatis实现MySQL的自动主从切换功能

《SpringBoot项目中结合MyBatis实现MySQL的自动主从切换功能》:本文主要介绍SpringBoot项目中结合MyBatis实现MySQL的自动主从切换功能,本文分步骤给大家介绍的... 目录原理解析1. mysql主从复制(Master-Slave Replication)2. 读写分离3.

idea maven编译报错Java heap space的解决方法

《ideamaven编译报错Javaheapspace的解决方法》这篇文章主要为大家详细介绍了ideamaven编译报错Javaheapspace的相关解决方法,文中的示例代码讲解详细,感兴趣的... 目录1.增加 Maven 编译的堆内存2. 增加 IntelliJ IDEA 的堆内存3. 优化 Mave

Java String字符串的常用使用方法

《JavaString字符串的常用使用方法》String是JDK提供的一个类,是引用类型,并不是基本的数据类型,String用于字符串操作,在之前学习c语言的时候,对于一些字符串,会初始化字符数组表... 目录一、什么是String二、如何定义一个String1. 用双引号定义2. 通过构造函数定义三、St