Reactor响应式编程系列(四)- 深入理解Schedulers.elastic()

Reactor响应式编程系列导航

前言

经过几轮学习,熟悉了FluxMono两种序列以及各种操作符的功能。那么现在应该将重心放在线程了。前几篇博客中的各种案例,其运行肯定是在线程上的,这没得跑,而在 Reactor 中,执行模式以及执行过程取决于所使用的 Scheduler。那么接下来就来学习下Scheduler吧。

一. Scheduler调度器

Scheduler是一个抽象接口,提供的静态方法用于创建如下几种执行环境:

  • 当前线程:Schedulers.immediate()
  • 可重用的单线程:Schedulers.single()

当各个调用者调用这个方法的时候,都会重用同一个线程,直到这个Scheduler的状态被设定为disposed。

  • 弹性线程池Schedulers.elastic(),该方法是一种将阻塞处理放在一个单独的线程中执行的很好的方式,即可以拿来包装一个堵塞方法,将其变为异步。同时这个方法也是最为复杂的一个方法。因此本篇文章着重来讲解Schedulers.elastic()

1.当首次调用这个方法的时候,会创建一个新的线程池,而且这个线程池中闲置的线程可以被重用。
2.如果一个线程的闲置时间太长(默认60s),则会被销毁。

  • 固定大小线程池:Schedulers.parallel()

该方法创建的线程数量取决于CPU的核数。

在正式讲解Schedulers.elastic()之前,还需要说明几点:

  • 某些Reactor的操作已经是默认使用了特定类型的调度器。例如Flux.interval()创建的源,使用了Schedulers.parallel()等。
  • 我们可以通过publishOn()subscribeOn()来切换执行操作的调度器。
  • publishOn()切换的是元素消费操作执行时所在的线程(会在后续文章中单独讲解)。
  • subscribeOn()切换的是源中元素生产操作执行时所在的线程(会在后续文章中单独讲解)。

1.1 Schedulers.elastic()

1.来看下这个方法:

public static Scheduler elastic() {
    return cache(CACHED_ELASTIC, "elastic", ELASTIC_SUPPLIER);
}
// elastic()最后调用了cache()方法
static Schedulers.CachedScheduler cache(AtomicReference<Schedulers.CachedScheduler> reference, String key, Supplier<Scheduler> supplier) {
	// 1.通过原子类AtomicReference来获取缓存的CachedScheduler,若没有则临时创建一个新的。
    Schedulers.CachedScheduler s = (Schedulers.CachedScheduler)reference.get();
    if (s != null) {
        return s;
    } else {
    	// 2.若已经存在该缓存执行器,则调用supplier.get()去进行懒加载。
        s = new Schedulers.CachedScheduler(key, (Scheduler)supplier.get());
        if (reference.compareAndSet((Object)null, s)) {
            return s;
        } else {
            s._dispose();
            return (Schedulers.CachedScheduler)reference.get();
        }
    }
}

2.可以发现,里面频繁出现CachedScheduler这个类,因此再来看下这个类有什么属性(Schedulers类的静态内部类):

static class CachedScheduler implements Scheduler, Supplier<Scheduler> {
    final Scheduler cached;
    final String key;
	// 结合上面的代码:s = new Schedulers.CachedScheduler(key, (Scheduler)supplier.get());
	// 可以发现key就是elastic,cached则是(Scheduler)supplier.get()
    CachedScheduler(String key, Scheduler cached) {
        this.cached = cached;
        this.key = key;
    }
    //..

可以发现,CachedScheduler引入适配器的设计模式,会根据key的类型对Scheduler进行适配。

3.紧接着来看下Schedulers类中的某一个静态属性:

static final Supplier<Scheduler> ELASTIC_SUPPLIER = () -> {
    return newElastic("elastic", 60, true);
};
↓↓↓
public static Scheduler newElastic(String name, int ttlSeconds, boolean daemon) {
   return newElastic(ttlSeconds, new Schedulers.SchedulerThreadFactory(name, daemon, ElasticScheduler.COUNTER));
}
↓↓↓
public static Scheduler newElastic(int ttlSeconds, ThreadFactory threadFactory) {
    return factory.newElastic(ttlSeconds, threadFactory);
}

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
在这里插入图片描述
可以发现最终得到的是一个ThreadFactory类型的实例,而我们一般对于线程的相关参数的定义也都是通过ThreadFactory接口来完成。

并且最终是通过ElasticScheduler这个类来完成构造。

二. ElasticScheduler

ElasticScheduler类实现了Scheduler接口,先来看下Scheduler接口有哪些重要的方法:

public interface Scheduler extends Disposable {
    Disposable schedule(Runnable var1);
	// 通过指定的延迟来执行对应的任务
    default Disposable schedule(Runnable task, long delay, TimeUnit unit) {
        throw Exceptions.failWithRejectedNotTimeCapable();
    }
	// 以给定的初始延迟和周期来定时调度指定任务
    default Disposable schedulePeriodically(Runnable task, long initialDelay, long period, TimeUnit unit) {
        throw Exceptions.failWithRejectedNotTimeCapable();
    }
	// 返回当前的时间
    default long now(TimeUnit unit) {
        return unit.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }

    Scheduler.Worker createWorker();
	// 在放弃调度器时,需要释放相关资源的方法
    default void dispose() {
    }
	// 执行任务
    default void start() {
    }
	//..
}

Reactor官方对其的注释是这样的:
在这里插入图片描述

  • 可以让操作异步化。
  • 使用底层ExecutorServiceScheduledExecutorService的实现类进行修饰。

再来看下ElasticScheduler的继承关系,是不是联系上了?

final class ElasticScheduler implements Scheduler, Supplier<ScheduledExecutorService> {

意思是,我们的Schedulers.elastic()方法需要ScheduledExecutorService的实现类来修饰。

那么ScheduledExecutorService的实现类又咋获得呢?它是通过Schedulers类下的静态方法decorateExecutorService()来获得。

2.1 获取ScheduledExecutorService实现类

来看下这个方法(Schedulers类中):

static ScheduledExecutorService decorateExecutorService(String schedulerType, Supplier<? extends ScheduledExecutorService> actual) {
    return factory.decorateExecutorService(schedulerType, actual);
}
↓↓↓
public interface Factory {
        default ScheduledExecutorService decorateExecutorService(String schedulerType, Supplier<? extends ScheduledExecutorService> actual) {
            return (ScheduledExecutorService)actual.get();
        }

我们知道ScheduledExecutorService它是一个定时调度线程池,最后的任务肯定是通过submit()方法来提交对吧?

但是使用过线程池的小伙伴都知道,这种对象的创建相对而言是比较麻烦的,毕竟还有7大线程池参数呢~,因此为了避免不断的创建这种大对象,设计了CachedService这个静态内部类来作为缓存。

2.1.1 用CachedService类作为缓存

该类在ElasticScheduler类中:

static final class CachedService implements Disposable {
    final ElasticScheduler parent;
    final ScheduledExecutorService exec;

    CachedService(@Nullable ElasticScheduler parent) {
        this.parent = parent;
        if (parent != null) {
        	// 若传入的ElasticScheduler实例非空,直接拿来复用
            this.exec = Schedulers.decorateExecutorService("elastic", parent);
        } else {
        	// 若传入的为空,则新创建一个。
            this.exec = Executors.newSingleThreadScheduledExecutor();
            this.exec.shutdownNow();
        }
    }
    // ..
}

decorateExecutorService()方法最后可以看到调用的是Supplier接口的get()方法,当然,最终肯定是由子类完成。而ElasticScheduler类实现了Supplier接口,重写了其get()方法:

public ScheduledExecutorService get() {
    return Executors.newSingleThreadScheduledExecutor(this.factory);
}

到这里我们能得出什么结论?

小总结1

  1. 前提:我们使用Reactor框架时,一般用Schedulers.elastic()来对一个阻塞方法进行包装,变成异步。
  2. 而该方法需要ScheduledExecutorService类(定时线程池)的实现类来进行修饰。
  3. 考虑到线程池创建的成本相对较高,用ElasticScheduler.CachedService来进行缓存。
  4. 每次想获取ScheduledExecutorService对应的实现类,都需要调用一次ElasticScheduler.get()方法。
  5. 结果是每次返回一个新的线程池。

但是,这个线程池里面却只有一个线程(当然啦,返回的是单线程池),因此 CachedService 除了需要对任务执行器进行的缓存,还需要做:超时释放管理。而这些操作都是在dispose()方法中去完成。

2.2 ElasticScheduler.schedule ()调度任务

ElasticScheduler作为调度器的角色,在讲dispose()方法前,先来看下其调度逻辑,其实现在schedule()方法中(以下内容都属于ElasticScheduler类):

final Queue<ElasticScheduler.ScheduledExecutorServiceExpiry> cache;
// 用于做移除线程工作的队列
final Queue<ElasticScheduler.CachedService> all;
↓↓↓
public Disposable schedule(Runnable task) {
    ElasticScheduler.CachedService cached = this.pick();
    // 4.紧接着作为参数传入这个方法Schedulers.directSchedule()
    return Schedulers.directSchedule(cached.exec, new ElasticScheduler.DirectScheduleTask(task, cached), 0L, TimeUnit.MILLISECONDS);
}
↓↓↓
ElasticScheduler.CachedService pick() {
    if (this.shutdown) {
        return SHUTDOWN;
    } else {
        ElasticScheduler.ScheduledExecutorServiceExpiry e = (ElasticScheduler.ScheduledExecutorServiceExpiry)this.cache.poll();
        // 1.在ElasticScheduler类进行给初始化的时候赋值为null 
		// 2.因此当第一次调用下面的pick()方法的时候,e的值为null,因此走else分支
        if (e != null) {
            return e.cached;
        } else {
        	// 3.显然这里是创建了一个CachedService实例并返回。
            ElasticScheduler.CachedService result = new ElasticScheduler.CachedService(this);
            this.all.offer(result);
            if (this.shutdown) {
                this.all.remove(result);
                return SHUTDOWN;
            } else {
                return result;
            }
        }
    }
}

我们可以看到Schedulers.directSchedule()方法中,除了传入的cache对象,还传入一个Runnable类型的任务(第二个参数)。

而该任务通过ElasticScheduler.DirectScheduleTask()来创建,它是对自定义任务进行包装的地方。在这给个用Reactor来包装阻塞方法的伪代码:

Mono.fromCallable(() -> getUser()).subscribeOn(Schedulers.elastic())

那这么一看:我们传入的阻塞方法getUser()就是通过DirectScheduleTask()方法来进行包装的。

2.2.1 ElasticScheduler.DirectScheduleTask()包装任务

1.我们来看下DirectScheduleTask这个类~:

static final class DirectScheduleTask implements Runnable {
    final Runnable delegate;
    final ElasticScheduler.CachedService cached;

    DirectScheduleTask(Runnable delegate, ElasticScheduler.CachedService cached) {
    	// 对传入的Runnable对象进行增强,赋值给内部的final对象
        this.delegate = delegate;
        this.cached = cached;
    }

    public void run() {
        try {
            this.delegate.run();
        } catch (Throwable var5) {
            Schedulers.handleError(var5);
        } finally {
        	// 最后会执行dispose方法,这里就接上了 小总结1 中末尾讲的话
            this.cached.dispose();
        }
    }
}

2.紧接着开始将dispose()方法:

public void dispose() {
	// 1.线程任务执行完毕后,需要判断cached.exec(ScheduledExecutorService 类型)是否为null。并且其状态不是shutdown状态。
	// 2.则对该ExecutorService设定闲置过期的效果 
    if (this.exec != null && this != ElasticScheduler.SHUTDOWN && !this.parent.shutdown) {
    	// 3.通过ScheduledExecutorServiceExpiry包装来来实现设定
    	// 就是做一个动作:判断过期时间
        ElasticScheduler.ScheduledExecutorServiceExpiry e = new ElasticScheduler.ScheduledExecutorServiceExpiry(this, System.currentTimeMillis() + (long)this.parent.ttlSeconds * 1000L);
        // 紧接着就将其添加到ElasticScheduler管理的缓存队列中,准备重用
        this.parent.cache.offer(e);
        if (this.parent.shutdown && this.parent.cache.remove(e)) {
            this.exec.shutdownNow();
        }
    }
}

2.2.2 Schedulers.directSchedule()管理Future

我们前面主要围绕2.2小节的注释4来进行展开,对其中的任务包装进行了讲解,回到最外层的方法,也就是directSchedule(),来看下它是做什么的:

// 传入的exec参数是上文的cached.exec,即线程池
// 传入的task指的是已经被包装好的任务,该任务在执行完毕后会调用dispose()方法,让线程池重用。
static Disposable directSchedule(ScheduledExecutorService exec, Runnable task, long delay, TimeUnit unit) {
    SchedulerTask sr = new SchedulerTask(task);
    Object f;
    // 很明显是根据传入的参数,也就是根据是否有时间参数来选择是直接运行这个任务还是周期性的运行该任务。
    if (delay <= 0L) {
        f = exec.submit(sr);
    } else {
        f = exec.schedule(sr, delay, unit);
    }
	// 将Future进行包装然后返回
    sr.setFuture((Future)f);
    return sr;
}

小总结2

2.2节主要讲了什么,我在这里做个概括:

  1. ElasticScheduler类除了负责缓存线程池,还负责任务的调度。ElasticScheduler类中保存了两个队列:cache缓存队列)和all移除队列
  2. schedule(task)方法执行任务调度时,首先会从缓存队列cache中去获得一个线程池,若队列中有则直接返回重用,若无则创建一个并返回。
  3. 紧接着调用Schedulers.directSchedule()方法,该方法做了两件事。

1.对传入的task进行包装处理,让其最后调用dispose()方法,对线程池设定闲置过期效果。
2.启动线程任务,并将获得的Future对象进行封装返回。

这里需要特别申明几点

  1. Schedulers.elastic()确确实实的在使用单线程池,但并不代表只能运行一个任务。
  2. Schedulers.elastic()可以包含的线程池数量是无限的,只不过每个线程池都有一个过期时间。
  3. 每个在队列中的线程池,只要没过期并且处于空闲状态,就能够被重用。
  4. 综合以上3点,所以Schedulers.elastic()也叫作弹性线程池。

大总结

本篇文章主要讲的是Schedulers.elastic()中任务的一个执行流程,这里在进行一个归纳:

  1. 一般使用Reactor来对一个阻塞方法进行异步封装时,我们使用Mono.fromCallable(() -> getUser()).subscribeOn(Schedulers.elastic())的方式进行封装。
  2. Schedulers.elastic()作为一个弹性线程池,其最终的构造由ElasticScheduler类来完成。
  3. ElasticScheduler类实现了Scheduler接口,因此有如下几个特性。

1.schedule()方法完成任务调度。
2.dispose()放弃调度,并释放资源。
3.可以使操作异步化,但是底层需要ExecutorService或者ScheduledExecutorService的实现类进行修饰。

  1. 同时ElasticScheduler类还实现了Supplier< ScheduledExecutorService >,因此连通上述第三点。ElasticScheduler需要获得ScheduledExecutorService 的实现类。
  2. 实现类通过decorateExecutorService()方法来获得,并且用CachedService静态内部类对其实例进行缓存,对应的实例是Single线程池。 存储于内部的cache队列中。
  3. 若要执行某个任务,则调用schedule(task)方法,以不含时间参数为例,做这么几个动作:

1.对传入的task进行包装,在任务执行完成之后通过finally块调用cached.dispose()方法。
2.dispose()方法主要是对ExecutorService进行包装,让其有过期的特性,并将其放入到缓存队列中以重用。

  1. 最终返回一个Disposable类型的对象,其中封装了Future对象,即可得最终的异步回调结果。
  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zong_0915

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值