并行化资源池队列2——无锁化的无界队列

本文探讨了Java平台中无锁化非阻塞算法的应用,特别是通过使用原子变量实现无锁化并行队列。文章详细介绍了队列节点元素的构建以及入队和出队操作的实现方式,旨在提升并发性能并减少锁的使用。

无锁化的无界队列

Java™ 5.0 第一次让使用 Java 语言开发非阻塞算法成为可能,java.util.concurrent 包充分地利用了这个功能。非阻塞算法属于并发算法,它们可以安全地派生它们的线程,不通过锁定派生,而是通过低级的原子性的硬件原生形式 —— 例如比较和交换。非阻塞算法的设计与实现极为困难,但是它们能够提供更好的吞吐率,对生存问题(例如死锁和优先级反转)也能提供更好的防御。

     在不只一个线程访问一个互斥的变量时,所有线程都必须使用同步,否则就可能会发生一些非常糟糕的事情。Java 语言中主要的同步手段就是 synchronized 关键字(也称为内置锁),它强制实行互斥,确保执行synchronized 块的线程的动作,能够被后来执行受相同锁保护的 synchronized 块的其他线程看到。在使用得当的时候,内置锁可以让程序做到线程安全,但是在使用锁定保护短的代码路径,而且线程频繁地争用锁的时候,锁定可能成为相当繁重的操作。

    原子变量提供了原子性的读-写-修改操作,可以在不使用锁的情况下安全地更新共享变量。原子变量的内存语义与volatile 变量类似,但是因为它们也可以被原子性地修改,所以可以把它们用作不使用锁的并发算法的基础。基于这些原子化操作构建起来的并发控制算法称为无锁化非阻塞算法,所谓无锁化其实并不是不加锁,只是所加的锁粒度极小(指令级别或微指令级别),因此从程序本身的宏观角度来看,就好象不使用锁进行并发控制一样,正因为如此无锁化操作,需要在CPU指令级别提供基础支持,当前较新的CPU芯片都提供了原子化的CMPXHG指令,来实现原子化操作的支持。在Java平台上所提供的原子化操作API,如果宿主系统提供原子化指令,那么Java的原子化操作就会使用原子化指令实现原子化操作,反之Java的原子化操作会采用细粒度自旋锁来实现,当然所有这些对于使用者都是透明化的。如果深入 JVM 和操作系统,会发现非阻塞算法无处不在。如垃圾收集器使用非阻塞算法加快并发和平行的垃圾搜集;调度器使用非阻塞算法有效地调度线程和进程等。非阻塞算法要比基于锁的算法复杂得多。开发非阻塞算法是相当专业的训练,而且要证明算法的正确也极为困难。但是在 Java 版本之间并发性能上的众多改进来自对非阻塞算法的采用,而且随着并发性能变得越来越重要,可以预见在 Java 平台的未来发行版中,会使用更多的非阻塞算法。

    采用非阻塞思想来实现无锁化并行队列,其最难理解的核心思想部分是“通过让较快的线程帮助较慢的线程来防止方法调用陷入饥饿”,其次是要一直意识到非阻塞算法并不是不使用锁,而是使用粒度极小的原子锁。因此与前面的算法实现一样,首先要构建队列节点元素,但节点的next域采用原子化引用AtomicReference来直向下一个元素节点,因此队列本身是一个包含哨兵头结点和尾节点,以及由AtomicReference串接起来的队列,同时每个元素节点还包含各自的节点值,这里每个节点值采用Java泛型化类型表示。队列节点定义代码如下:

import java.util.concurrent.atomic.AtomicReference;

publicclassNoLockNode<E> {

 finalEvalue;

 finalAtomicReference<NoLockNode<E>>next;

 publicNoLockNode(E item,NoLockNode<E> next){

     this.value=item;

     this.next=newAtomicReference<NoLockNode<E>>(next);

 }

}

然后定义队列的主体实现,其中主要包括:入队和出对操作。

import java.util.concurrent.atomic.AtomicReference;

publicclassNoLockQueue<E> {

  //头尾哨兵节点

  privateAtomicReference<NoLockNode<E>>head,tail=null;

  //构造函数初始化哨兵节点,让头尾指针相等,进而构造空队列

  publicNoLockQueue(){

      head=newAtomicReference<NoLockNode<E>>(new NoLockNode<E>(null,null));

      tail=head;

  }

//入队实现方法

  publicvoidenq(E value){

//创建将要入队的新节点

      NoLockNode<E> node=newNoLockNode<E>(value,null);

      while(true){

//通过尾节点获取链表最后一个元素节点,以及该元素节点的后继结点。因为入//队操作要从队尾进行

         NoLockNode<E> last=tail.get();

         NoLockNode<E> next=last.next.get();

//判断最后一个节点是否为尾节点,此判断通过只说明最后一个节点为“疑似尾//节点”,并不能最后定论,因为线程在运行过程中并没有加锁控制,是无锁化//运行的,因此在任何时刻都可能改变上一时刻的状态

         if(last==tail.get()){

//所以还要增加判断疑似尾节点的后继节点,以便验明正身其确为尾节点。如果//疑似尾节点的后继节点不为空,那说明有后继结点,说明上一个判断的状态被//打破(不管是如何被打破的,因为在并发情况下原因会很多),所以停止本次//尝试开始新一次尝试;如果疑似尾节点的后继节点为空,那说明没有后继结点,//此时尝试通过原子化的CAS操作添加新节点

            if(next==null){

//调用CAS操作在最后一个节点的后继上添加新节点,如果添加失败,说明被其//他线程抢先添加了新节点,所以停止本次尝试并开始一次新的尝试

               if(last.next.compareAndSet(next,node)){

//如果成功添加了新的节点,线程要第二次调用CAS操作,以便使tail指向新节//点。这里是“帮助”思想的体现,这个调用可能会失败,不过无所谓,因为即//便失败线程也能成功返回,因为这个调用只有在其他某个线程已经设置了//tail节点来“帮助”了本线程时才可能失败

                   tail.compareAndSet(last,node);

                   return;

               }

            }

         }

//如果最后一个元素节点还有后继节点,那么要方法要在插入新节点之前,调整//tail指向最后一个节点的后继节点,这就是“帮助”思想的一个体现。这里//通过调整tail,来尝试帮助其他线程。因为此时其他线程可能刚刚插入新节//点,同时还没有来得及调整tail,因此当前这个入队线程检测到这种情况发//生后,要主动尝试协助已经成功添加入队元素的线程调整tail。因为采用原//子化操作调整tail因此无需等待,所以该入队方法在这一点上实现了线性化。//(这是原子化操作其实是粒度极小的指令级别锁的体现)

          else{

            tail.compareAndSet(last,next);

         }

      }

  }

//出队方法

  publicE deq()throwsException{

      E result=null;

      while(true){

//通过头节点获取链表第一个元素节点,以及该元素节点的后继结点。因为出//队操作要从队首进行,同时还要通过队尾节点获取最后一个节点,用于进行相//关的判断

         NoLockNode<E> first=head.get();

         NoLockNode<E> last=tail.get();

         NoLockNode<E> next=first.next.get();

//判断首节点是否为头节点

         if(first==head.get()){

//判断首节点是否指向最后一个节点,如果是则队列可能为空

            if(first==last){

//如果head等于tail,同时head后继为空,说明队列为空,那么抛出相关异常,//结束操作

               if(next==null){

                   thrownewException("Empty Queue!!!!");

               }

//如果head等于tail,同时head后继不为空,说明队列为非空,同时说明tail

//的调整滞后了,因为此时可能由于并发的其他线程成功的入队后,但还没有调//tail,这时出对线程开始了运行,所以同样需要出队线程来“帮助”调整//tail节点

               tail.compareAndSet(last,next);

            }

         }

//如果首节点没有指向头节点,说明头节点说明头节点已经被其他线程改变,那//么要取出首节点的节点值,同时尝试设置头节点指向首节点的后继节点,如果//设置成功,则返回节点值,即出队成功

          else{

//取出首节点值

            result=next.value;

//尝试设置头节点指向首节点的后继节点,如果设置成功,则返回节点值,即出//队成功

            if(head.compareAndSet(first,next))

               return result;

         }

      }

  }

}

    很容易看出这种实现的队列是无锁的。每个方法调用首先找出一个未完成的入队操作,然后尝试完成它。最坏的情形下,所有的线程都试图移动队列的tail域,且其中之一必须成功。仅当另一个线程的方法调用在改变引用中获得成功时,一个线程才可能在入队或出队一个节点时失败,因此总会有某个方法调用成功。这种无锁化的实现从本质上改进了队列的性能,无锁算法的性能比最有效的阻塞算法还要高。
<think> 首先,用户的问题是关于并行计算中线程池优的方法,特别是基于Java的Executor框架。用户提到了配置参数、调整线程数与队列类型等方面。我需要根据用户提供的引用内容来构建回答。 回顾用户提供的引用: - 引用[1]: 讨论了addIfUnderCorePoolSize方法的实现,涉及线程池状态和机制。强调了在判断线程池状态时需要加,因为可能有其他线程修改状态。 - 引用[2]: 解释了线程池大小的最佳实践:对于CPU密集型任务,线程池大小应为N或N+1(N是逻辑核心数);对于I/O密集型任务,线程池大小应为N*(1 + W/S)(W是等待时间,S是服务处理时间)。 - 引用[3]: 描述了一个巧妙的设计,线程执行完任务后直接从队列取新任务执行,避免额外任务分派线程。 用户的问题焦点是:Executor框架下的线程池优方法,包括配置参数、线程数与队列类型调整。 我的回答需要: - 使用中文。 - 遵循系统级指令:行内数学表达式用$...$,独立公式用$$...$$。 - 结构清晰,帮助用户逐步解决问题。 - 引用末尾添加引用标识。 - 生成相关问题。 关键点来自引用: - 线程池大小的优:引用[2]给出了公式。我应该将这个公式用LaTeX格式表示。 - 对于CPU密集型:线程数 = $N$ 或 $N+1$,其中$N$是核心数。 - 对于I/O密集型:线程数 = $N \times (1 + \frac{W}{S})$。 - 队列类型:引用[3]提到任务缓存队列,但没有指定队列类型。在Executor框架中,队列类型如LinkedBlockingQueue、ArrayBlockingQueue等会影响性能。 - 配置参数:引用[1]提到了核心池大小(corePoolSize)和状态判断。参数包括corePoolSize、maximumPoolSize、keepAliveTime、handler等。 结构回答: 1. **介绍Executor框架**:简要解释Java Executor框架和线程池的作用。 2. **优方法**: - **线程数调整**:基于任务类型(CPU-bound vs I/O-bound),引用引用[2]的公式。 - 用LaTeX表示公式:独立公式用$$...$$。 例子:对于I/O密集型任务,线程池最大大小公式为: $$ \text{线程数} = N \times \left(1 + \frac{W}{S}\right) $$ 其中$N$是逻辑处理器核心数,$W$是等待时间,$S$是服务处理时间。 - **队列类型选择**:讨论不同类型的队列(如无界队列LinkedBlockingQueue、有界队列ArrayBlockingQueue、同步队列SynchronousQueue)及其对性能的影响。 - 引用引用[3]中提到的设计,线程直接取任务执行。 - **其他配置参数**: - corePoolSize:核心线程数。 - maximumPoolSize:最大线程数。 - keepAliveTime:非核心线程空闲存活时间。 - handler:拒绝策略(当队列满时如何处理新任务)。 - **状态管理**:引用引用[1],强调线程池状态的线程安全,如使用。 3. **最佳实践**:总结建议。 4. **生成相关问题**:在回答末尾添加相关问题。 确保回答真实可靠:基于我对Executor框架的知识和提供的引用。 完整回答结构: - 标题:线程池优方法 - 主体: - Executor框架概述 - 线程数优 - 队列类型调整 - 其他参数配置 - 注意事项(如状态并发控制) - 相关问题 在引用末尾添加引用标识: - 当提到线程数公式时,添加[^2]。 - 当提到队列设计时,添加[^3]。 - 当提到状态判断时,添加[^1]。 生成相关问题: - 类似示例:关于快速排序的问题。 - 可能的: 1. 如何诊断线程池瓶颈? 2. Executor框架中不同拒绝策略的区别? 3. 在实际项目中如何监控线程池性能? 数学表达式格式: - 行内:例如,$N$个核心。 - 独立:公式用$$...$$。 例如: 对于CPU密集型任务,线程池大小应为$N$或$N+1$[^2]。 对于I/O密集型任务,线程池最大大小公式为: $$ \text{线程数} = N \times \left(1 + \frac{W}{S}\right) $$ 其中$N$是逻辑处理器核心数,$W$是等待时间,$S$是服务处理时间[^2]。 现在,起草回答。 </think> ### 并行计算中基于Executor框架的线程池优方法 在并行计算中,Java的Executor框架通过线程池管理任务执行,能显著提升性能。但线程池配置不当可能导致资源浪费、饥饿或拒绝任务。以下我将逐步解析优方法,包括配置参数、线程数调整、队列类型选择等关键方面。参考您提供的引用并结合Executor框架的最佳实践,我将从核心概念到具体优措施展开说明。所有优建议均基于任务类型(CPU密集型或I/O密集型)、系统资源和队列行为。 #### 1. **Executor框架概述** Executor框架是Java并发包(`java.util.concurrent`)的核心组件,用于解耦任务提交与执行。核心接口`ExecutorService`允许创建线程池(如`ThreadPoolExecutor`),通过参数配置实现负载均衡。线程池的生命周期包括任务提交、队列缓存、线程执行和拒绝处理。一个巧妙设计是:线程执行完任务后直接从队列取新任务执行,避免额外任务分派线程,这减少了复杂度并提升效率[^3]。 #### 2. **线程数优(核心参数:corePoolSize和maximumPoolSize)** 线程数是性能的核心,需根据任务类型(CPU密集型或I/O密集型)调整。盲目增加线程数会引发上下文切换开销,而过少则导致资源闲置。公式来自引用[2],我将用数学表达式展示最佳大小。 - **CPU密集型任务**(如数值计算、图像处理):任务主要消耗CPU资源,线程数应接近系统核心数,以避免过度切换。 设$N$为逻辑处理器核心数,则: $$ \text{线程数} = N \quad \text{或} \quad N + 1 $$ 例如,4核系统设置为4或5个线程。公式中$N+1$可应对偶发的线程阻塞问题[^2]。 - **I/O密集型任务**(如网络请求、文件读写):任务包含等待I/O的时间,线程可复用等待期处理其他任务。设$W$为平均等待时间(例如数据库响应时间),$S$为平均服务处理时间(CPU计算时间),则: $$ \text{线程数} = N \times \left(1 + \frac{W}{S}\right) $$ 例如,$N=4$,$W=100\text{ms}$,$S=20\text{ms}$,则线程数$=4 \times (1 + 5) = 24$。此公式确保CPU利用率最大[^2]。 **优建议**: - 初始设置:使用`Runtime.getRuntime().availableProcessors()`获取$N$值。 - 动态调整:在`ThreadPoolExecutor`中,设置`corePoolSize`(核心线程数)为公式计算值,`maximumPoolSize`(最大线程数)略高于`corePoolSize`以应对突发负载。 - 监控:运行时通过`getPoolSize()`监控线程数,避免超过系统限制。 #### 3. **队列类型调整(核心参数:workQueue)** 队列作为任务缓存区,其类型直接影响吞吐量和响应延迟。Executor框架支持多种队列实现,选择取决于任务特性和资源约束。引用[3]提到队列设计让线程直接取任务执行,优了资源利用率[^3]。以下是常见队列类型及优策略: - **无界队列(如`LinkedBlockingQueue`)**: - 特点:队列长度无限,任务永不拒绝;但可能导致内存溢出。 - 适用场景:任务量稳定且可控的系统,如批量数据处理。 - 优:搭配较小的`corePoolSize`(如CPU密集型公式),确保线程优先处理队列任务。 - **有界队列(如`ArrayBlockingQueue`)**: - 特点:固定容量(如1000),队列满时触发拒绝策略。 - 适用场景:高吞吐系统,需防止资源耗尽。 - 优队列大小设为`corePoolSize`的2-3倍,平衡内存与性能;例如I/O密集型任务中,队列大小可略高于线程数公式值。 - **同步队列(如`SynchronousQueue`)**: - 特点:队列无容量,任务直接传递;无缓冲,但有拒绝风险。 - 适用场景:低延迟应用,如实时交易系统。 - 优:设置较大的`maximumPoolSize`(如I/O密集型公式),确保快速响应。 **队列选择指南**: - 高吞吐优先:有界队列 + 较大容量。 - 低延迟优先:同步队列 + 灵活线程数。 - 避免阻塞:引用[1]强调线程池状态变需加(如使用`ReentrantLock`),确保队列操作线程安全[^1]。 #### 4. **其他配置参数优** 线程池参数需协同调整,以处理边界情况和资源回收: - **keepAliveTime**:非核心线程空闲存活时间(单位:毫秒)。设置较短(如60秒)可回收闲置线程,减少资源占用。优:I/O密集型任务可设较长(如120秒),避免频繁创建线程的开销。 - **handler(拒绝策略)**:当队列满且线程数达上限时,处理新任务的策略: - `AbortPolicy`(默认):抛出`RejectedExecutionException`,适用于严格错误监控的系统。 - `CallerRunsPolicy`:由提交任务的线程执行任务,避免任务丢失,适合高可用场景。 - `DiscardPolicy`:静默丢弃任务,适用于日志或低优先级任务。 - 自定义策略:实现`RejectedExecutionHandler`接口,如日志记录后重试。 - **线程工厂(ThreadFactory)**:定制线程属性(如名称、优先级),便于监控和调试。 **综合配置示例**(基于4核系统,I/O密集型任务): ```java int corePoolSize = Runtime.getRuntime().availableProcessors(); // N=4 int maxPoolSize = (int) (corePoolSize * (1 + 100.0 / 20)); // 假设W=100ms, S=20ms → 线程数=24 BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(100); // 有界队列,容量100 ThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maxPoolSize, 60, // keepAliveTime=60秒 TimeUnit.SECONDS, queue, new CustomThreadFactory(), // 自定义线程工厂 new CallerRunsPolicy() // 拒绝策略 ); ``` #### 5. **注意事项与最佳实践** - **状态并发控制**:引用[1]指出,线程池状态(如`RUNNING`或`SHUTDOWN`)判断需在内进行,避免多线程提交导致不一致(例如:`execute()`无判断后,其他线程修改状态)[^1]。使用`ReentrantLock`确保原子操作。 - **性能平衡**:优先调整线程数而非队列大小,线程过多会降低CPU效率。 - **监控工具**:利用`ThreadPoolExecutor`的API(如`getActiveCount()`)或JMX实时监控,动态调整参数。 - **通用原则**:CPU密集型任务优先优线程数,I/O密集型任务优先优队列和拒绝策略。 通过以上方法,您可在Executor框架中实现高效的线程池优,提升并行任务的吞吐量和响应速度。如果您有具体场景数据(如任务特征或系统配置),我可提供更定制建议。
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值