JUC组件扩展之FutureTask

本文深入解析FutureTask组件、Fork/Join框架与BlockingQueue接口在Java并发编程中的应用,涵盖FutureTask的创建与使用,Fork/Join的工作窃取算法及其实现,以及BlockingQueue的多种实现类和适用场景。

1.概述

FutureTask这个组件是JUC里边的但是它不是AQS的子类;创建线程有两种方式,一种是继承Thread,一种是实现Runnable接口,这两种方式有一个共同的缺陷就是执行完任务以后无法获取执行结果,从JDK1.5开始就提供了Future和Callable通过他们可以在任务执行完毕以后获取结果;

2.Callable Future FutureTask使用方法

① Callable与Runnable接口的对比

Runnable的代码非常简单,它是一个接口而且只有一个方法run();创建一个类实现它并在里边写一些操作,然后使用线程去执行该Runnable实现类即可以实现多线程;

②Callable的代码也非常简单,不同的是它是一个泛型的接口它里边有一个call()方法,call()方法的返回值类型就是我们创建Callable时传进去的V类型,Callable与Runnable的功能大致相似;相比较而言Callable比Runnable稍微强大一些,因为Callable对线程执行后可以有返回值并且可以抛出异常;

③Future也是一个接口,Future可以取消任务,查询任务是否被取消以及获取结果等操作;通常线程都是异步计算模型的,我们不能直接从别的线程中得到方法的返回值。这个时候我们就可以使用Future来完成;Future可以监视目标线程调用call()的情况,当我们调用Future的get()方法的时候就可以获取到结果,这个时候通常线程可能不会直接完成,当前线程就会开始阻塞,直到call()方法结束,返回结果线程才会继续执行,Future可以得到别的线程任务的方法的返回值。

④ FutureTask它的父类是RunnableFuture,而RunnableFuture继承了Runnable与Future这两个接口,由此我们可以直到FutureTask最终也是执行Callable类型的任务,如果构造函数参数是Runnable的话,它会转换成Callable类型,所以FutureTask即可以作为Runnable被线程执行也可以作为Future拿到线程的执行结果;

3.代码演示

// Future演示
@Slf4j
public class FutureExample {

    static class MyCallable implements Callable<String> {

        @Override
        public String call() throws Exception {
            log.info("do something in callable");
            Thread.sleep(5000);
            return "Done";
        }
    }

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        Future<String> future = executorService.submit(new MyCallable());
        log.info("do something in main");
        Thread.sleep(1000);
        String result = future.get();
        log.info("result:{}", result);
    }
}

// FutureTask代码演示
@Slf4j
public class FutureTaskExample {

    public static void main(String[] args) throws Exception {
        FutureTask<String> futureTask = new FutureTask<String>(new Callable<String>() {
            @Override
            public String call() throws Exception {
                log.info("do something in callable");
                Thread.sleep(5000);
                return "Done";
            }
        });

        new Thread(futureTask).start();
        log.info("do something in main");
        Thread.sleep(1000);
        String result = futureTask.get();
        log.info("result:{}", result);
    }
}

 

3.Fork/Join

ForkJoin框架是JDK7提供的一个,用于并行执行任务的框架,他将一个大任务分割成若干个小任务,最终汇总每个小任务的结果后,得到大任务的结果;从字面上看Fork就是切割任务,Join就是合并结果并得到最终的结果;它主要采取的是工作窃取算法,工作窃取算法主要是指某个线程从其他队列里窃取任务来执行;

工作窃取流程图

 

为什么要使用工作窃取算法

原理 :

假如我们需要做一个比较大的任务,我们可以把这个任务分割成互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,为每一个队列创建一个单独的线程,来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A线程里的任务,但是有一些线程会先于其他线程完成任务,这个时候其他线程对应的队列里还有任务等待处理,此时就需要使用工作窃取,来让空闲的线程获取别的线程的任务;但是这个时候多个线程会访问同一个队列,为了减少窃取任务线程与被窃取任务线程之间竞争通常我们会使用双端队列,被窃取任务的线程永远从双端队列的头部拿任务进行处理,而窃取任务的线程从双端队列的底部获取任务执行;

优点 :

这个工作窃取算法的优点就是充分利用线程进行并行计算并减少了线程间的竞争;

缺点 :

它的缺点是在个别情况下还是会存在竞争(比如双端队列里只有一个任务时),同时这样也消耗了更多的系统资源(创建了多个线程和多个双端队列)

工作窃取算法在ForkJoin中的应用 :

对于ForkJoin框架而言,当一个任务正在等待它使用Join操作创建的子任务结束时,执行这个任务的工作线程查找其他未被执行的任务并开始执行,通过这种方式线程充分利用他们的运行时间来提高应用系统的性能,

ForkJoin的局限性 :

由于ForkJoin框架使用了工作窃取算法所以执行的任务有一些局限性;

① 首先任务只能使用Fork和Join操作来实现同步机制,如果使用了其他同步机制,他们在工作时工作线程就不能执行其他任务了;比如在ForkJoin框架中使任务进入休眠状态,那么在休眠期间正在执行的工作线程就不会执行其他任务了;

② 我们拆分的任务不应该去执行IO操作(如读或者写数据文件);

③ 任务不能抛出检查异常,必须通过必要的代码来处理他们;

ForkJoin框架的核心 :

ForkJoinPool : 负责做实现(包括实现工作窃取算法),它管理工作线程和提供关于任务的状态以及执行信息;

ForkJoinTask : 则主要提供在任务中执行Fork和Join操作的机制;

代码演示:

// ForkJoinTask代码演示
@Slf4j
public class ForkJoinTaskExample extends RecursiveTask<Integer> {

    public static final int threshold = 2;
    private int start;
    private int end;

    public ForkJoinTaskExample(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;

        //如果任务足够小就计算任务
        boolean canCompute = (end - start) <= threshold;
        if (canCompute) {
            for (int i = start; i <= end; i++) {
                sum += i;
            }
        } else {
            // 如果任务大于阈值,就分裂成两个子任务计算
            int middle = (start + end) / 2;
            ForkJoinTaskExample leftTask = new ForkJoinTaskExample(start, middle);
            ForkJoinTaskExample rightTask = new ForkJoinTaskExample(middle + 1, end);

            // 执行子任务
            leftTask.fork();
            rightTask.fork();

            // 等待任务执行结束合并其结果
            int leftResult = leftTask.join();
            int rightResult = rightTask.join();

            // 合并子任务
            sum = leftResult + rightResult;
        }
        return sum;
    }

    public static void main(String[] args) {
        ForkJoinPool forkjoinPool = new ForkJoinPool();

        //生成一个计算任务,计算1+2+3+4
        ForkJoinTaskExample task = new ForkJoinTaskExample(1, 100);

        //执行一个任务
        Future<Integer> result = forkjoinPool.submit(task);

        try {
            log.info("result:{}", result.get());
        } catch (Exception e) {
            log.error("exception", e);
        }
    }
}

 

5、BlockQueue

概述

BlockingQueue意思是阻塞队列,从阻塞这个词我们就可以看出来在某些情况下对阻塞队列的访问可能会造成阻塞;

 

被阻塞的情况

① 当队列已经满了的情况下的入队列操作(除非有另一个线程进行了出队列操作);

② 当队列已经空了的情况下的出队列操作(除非有另一个线程进行了如队列操作);

适用场景

通过上边的介绍以及阻塞队列的特性我们可以得出阻塞队列是线程安全的,它主要使用在生产者消费者的场景;

阻塞队列的常用方法

 

它提供了4套方法,大家可以根据自己的实际场景来选择;

阻塞队列的实现类

ArrayBlockingQueue : ArrayBlockingQueue是一个有界的阻塞队列,它的内部实现是一个数组,它的容量是有限的,我们必须在初始化的时候指定容量的大小且指定的大小值在指定以后就不能在更改,它是以先进先出的方式存储数据的,最新插入的对象是尾部最新移除的对象是头部;

DelayQueue : DelayQueue阻塞的是内部元素,DelayQueue中的元素必须实现Delayed接口,Delayed接口继承了Comparable接口(因为DelayQueue中的元素需要进行排序),一般情况下都是按照元素的过期时间优先级进行排序,DelayQueue的应用场景比如定义关闭连接,缓存对象,超时处理等多种场景,它的内部实现用的是lock(锁)与PriorityQueue(排序)

LinkedBlockingQueue : 它的大小是可选的,如果初始化时指定了大小那么它就是有边界的如果不指定就是无边界的(其实使用的默认的最大的整型值),它内部实现是一个链表,处理底层的结构不一样其他的都与ArrayBlockingQueue一样;它也是以先进先出的方式存储数据,最新插入的对象在尾部最新移除的对象在头部,

PriorityBlockingQueue : 它是一个带有优先级的阻塞队列且它是一个没有边界的队列但是它是有排序规则的,PriorityBlockingQueue是允许插入null的,在使用的PriorityBlockingQueue的时候需要注意,所以插入的对象必须实现Compilable接口,队列优先级的排序规则就是按照我们对这个接口的实现来定义的,我们可以从PriorityBlockingQueue获取一个迭代器,但是这个迭代器并不保证按照我们的迭代器进行迭代;

SynchronousQueue : 这个队列内部仅允许容纳一个元素,当一个线程插入一个元素后就会被阻塞,除非这个元素被另一个线程消费,因此我们又称之为同步队列,它是一个无界非缓存的队列准确的说它不存储元素,放入的元素之后等待取走以后才能继续放入;

⑥ BlockingQueue总结

它不仅实现了一个完整队列所具有的基本功能同时在多线程环境下还自动了管理多线程间的自动等待 唤醒功能,从而使开发人员可以忽略这些细节关注更高级的功能;

 

 

 

 

考虑柔性负荷的综合能源系统低碳经济优化调度【考虑碳交易机制】(Matlab代码实现)内容概要:本文围绕“考虑柔性负荷的综合能源系统低碳经济优化调度”展开,重点研究在碳交易机制下如何实现综合能源系统的低碳化与经济性协同优化。通过构建包含风电、光伏、储能、柔性负荷等多种能源形式的系统模型,结合碳交易成本与能源调度成本,提出优化调度策略,以降低碳排放并提升系统运行经济性。文中采用Matlab进行仿真代码实现,验证了所提模型在平衡能源供需、平抑可再生能源波动、引导柔性负荷参与调度等方面的有效性,为低碳能源系统的设计与运行提供了技术支撑。; 适合人群:具备一定电力系统、能源系统背景,熟悉Matlab编程,从事能源优化、低碳调度、综合能源系统等相关领域研究的研究生、科研人员及工程技术人员。; 使用场景及目标:①研究碳交易机制对综合能源系统调度决策的影响;②实现柔性负荷在削峰填谷、促进可再生能源消纳中的作用;③掌握基于Matlab的能源系统建模与优化求解方法;④为实际综合能源项目提供低碳经济调度方案参考。; 阅读建议:建议读者结合Matlab代码深入理解模型构建与求解过程,重点关注目标函数设计、约束条件设置及碳交易成本的量化方式,可进一步扩展至多能互补、需求响应等场景进行二次开发与仿真验证。
<think>嗯,用户让我介绍一下JUC里的Exchanger,我得先回忆一下相关知识。Exchanger是Java并发工具中的一个类,主要用于两个线程之间交换数据。可能用户已经对CountDownLatch、CyclicBarrier这些有所了解,现在想扩展到更少见的同步工具。 首先,我需要确定Exchanger的基本概念。它允许两个线程在某个同步点交换彼此的数据。比如,一个线程生产数据,另一个消费,他们可以用Exchanger来交换数据。这点需要明确说明,可能用户需要知道使用场景。 然后,核心方法应该是exchange(),这个方法会阻塞直到另一个线程也调用它。可能还有带超时的版本。这里要详细说明方法的使用方式,以及可能抛出的异常,比如中断或超时。 接下来,需要考虑它的实现原理。记得Exchanger使用了类似双重槽位的设计,通过CAS操作来保证线程安全。可能涉及到LockSupport的park和unpark,或者自旋等待。这部分可能需要深入源码分析,但用户可能不需要太底层的细节,但作为超详细介绍,还是得提到。 使用场景方面,除了生产者-消费者,还有线程间数据交换的其他例子,比如游戏服务器中的玩家交易,或者管道化处理的不同阶段交换数据。需要举一些实际的例子,让用户更容易理解。 注意事项方面,线程数超过两个的情况,Exchanger可能不适用,会出问题。另外,exchange()的调用时机需要协调,否则可能线程一直阻塞。还要提醒处理中断和超时的情况,避免程序挂死。 可能用户是正在学习Java并发编程的开发者,需要实际应用Exchanger解决问题,或者面试中遇到相关问题。需要给出一个简单的代码示例,比如两个线程交换字符串,这样直观明了。 还要比较Exchanger和其他同步工具的区别,比如和SynchronousQueue的不同。SynchronousQueue是传递数据,而Exchanger是双向交换,这可能容易混淆,需要澄清。 总结的时候,要强调Exchanger的适用场景和限制,帮助用户判断何时使用。另外,提醒线程间协作的必要性,以及正确使用的重要性,避免常见错误。</think>JUC之Exchanger超详细解析(附核心源码分析) Exchanger是Java并发包(java.util.concurrent)中用于两个线程间交换数据的同步工具类。以下是分步骤详解: 一、核心特性 1. 双向数据交换 - 仅支持两个线程间的数据交换(❗超过两个线程会抛出异常) - 线程A调用exchange()时会阻塞,直到线程B也调用exchange() - 数据交换完成后,双方线程继续执行 2. 方法签名 ```java public V exchange(V x) throws InterruptedException public V exchange(V x, long timeout, TimeUnit unit) throws InterruptedException, TimeoutException ``` 二、底层实现原理(基于JDK17) 1. 核心数据结构 ```java private static final class Node { int index; // 索引编号 int bound; // 边界值 int collides; // CAS失败次数 int hash; // 伪随机数 Object item; // 当前线程携带的数据 volatile Object match; // 配对线程返回的数据 volatile Thread parked; // 挂起的线程 } ``` 2. 交换流程 - 使用`Slot`数组实现多参与者模式(避免竞争) - 采用CAS+自旋实现无锁化操作 - 当发生哈希冲突时,通过`bound`和`collides`字段进行扩容控制 三、使用场景示例 ```java // 生产者-消费者模型 Exchanger<String> exchanger = new Exchanger<>(); // 生产者线程 new Thread(() -> { try { String data = "Product-"+System.currentTimeMillis(); String received = exchanger.exchange(data); System.out.println("Producer received: " + received); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }).start(); // 消费者线程 new Thread(() -> { try { String data = "Money-$100"; String received = exchanger.exchange(data); System.out.println("Consumer received: " + received); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }).start(); ``` 四、关键注意事项 1. 线程数量限制 - 严格仅支持两个线程交换数据 - 第三个线程调用exchange()会导致`IllegalStateException` 2. 超时控制 - 建议使用带超时的exchange方法 - 防止线程永久阻塞(默认最大等待时间约273年) 3. 中断处理 - 当线程被中断时抛出InterruptedException - 需要正确处理中断状态 五、性能优化技巧 1. 避免频繁创建 - Exchanger实例可重复使用 - 初始化成本较高(涉及内部数组的创建) 2. 对象复用 - 尽量复用交换对象(减少GC压力) - 推荐使用不可变对象 六、与SynchronousQueue对比 | 特性 | Exchanger | SynchronousQueue | |--------------------|-------------------|------------------| | 数据方向 | 双向交换 | 单向传递 | | 线程数量 | 严格两个 | 多个生产/消费者 | | 数据结构 | 无缓冲 | 可选公平策略 | | 适用场景 | 对等数据交换 | 生产-消费管道 | 七、源码级优化点 1. 消除伪共享 - 使用`@jdk.internal.vm.annotation.Contended`注解 - 对Node类进行缓存行填充(避免CPU缓存失效) 2. 哈希算法优化 ```java // 使用Marsaglia XorShift随机数算法 private static final int getProbe() { return ThreadLocalRandom.current().nextInt(); } ``` 常见问题解决方案: 1. 线程阻塞问题 - 使用`exchange(V x, long timeout, TimeUnit unit)`方法 - 设置合理的超时阈值 2. 数据竞争问题 - 建议交换不可变对象 - 如果必须使用可变对象,需要额外同步机制 适用场景建议: ✅ 游戏服务器玩家交易系统 ✅ 双线程数据校验核对 ✅ 管道式处理中间交换 ❌ 高吞吐量数据交换(推荐使用Disruptor)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值