SpringBoot官方支援任務排程框架,輕量級用起來也挺香!

沉默王二發表於2022-06-08

大家好,我是二哥呀。定時任務的應用場景其實蠻常見的,比如說:

  • 資料備份
  • 訂單未支付則自動取消
  • 定時爬取資料
  • 定時推送資訊
  • 定時釋出文章
  • 等等(想不出來了,只能等等來湊,?,反正只要等的都需要定時,怎麼樣,這波圓場可以吧)

程式設計喵?實戰專案裡需要做一個定時釋出文章的功能,一開始我想用 Spring Task,於是研究了一番,發現 Spring Task 用起來確實簡單,但對於複雜業務卻也無能為力。

於是我就把注意力放到了 Quartz 上面,這是一款老而彌堅的開源任務排程框架。

記得我在 14 年開發大宗期貨交易平臺的時候就用到了它,每天凌晨定時需要統計一波交易資料,生成日報報表,當時配合 Cron 表示式用的。

可惜後來平臺穩定了,新的政策出來了,直接把大宗期貨交易滅了。於是我發財的機會也隨著破滅了。想想都覺得可惜,哈哈哈。

時光荏苒,Quartz 發展到現在,已經可以和 Spring Boot 專案無縫銜接了,今天我們就來實戰一把。

Timer

JDK 1.3 就開始支援的一種定時任務的實現方式。內部通過 TaskQueue 的類來存放定時任務,用起來比較簡單,但缺陷比較多,比如說一個 Timer 就會起一個執行緒,任務多了效能就非常差,再比如說如果執行任務期間某個 TimerTask 耗時比較久,就會影響其他任務的排程。

@Slf4j
public class TimerDemo {
    public static void main(String[] args) {
        TimerTask task = new TimerTask() {
            @Override
            public void run() {
                log.debug("當前時間{}執行緒名稱{}", DateTime.now(),
                        Thread.currentThread().getName());
            }
        };
        log.debug("當前時間{}執行緒名稱{}", DateTime.now(),
                Thread.currentThread().getName());
        Timer timer = new Timer("TimerDemo");
        timer.schedule(task,1000L);
    }
}

程式碼跑起來後的日誌如下所示:

13:11:45.268 [main] DEBUG top.springtask.TimerDemo - 當前時間2022-04-27 13:11:45執行緒名稱main
13:11:46.280 [TimerDemo] DEBUG top.springtask.TimerDemo - 當前時間2022-04-27 13:11:46執行緒名稱TimerDemo

ScheduledThreadPoolExecutor

JDK 1.5 開始提供的的定時任務,它繼承了 ThreadPoolExecutor,實現了 ScheduledExecutorService 介面,所以支援併發場景下的任務執行。同時,優化了 Timer 的缺陷。不過,由於使用了佇列來實現定時器,就有出入佇列、調整堆等操作,所以定時不是非常非常準確(吹毛求疵)。

@Slf4j
public class ScheduledThreadPoolExecutorDemo {
    public static void main(String[] args) throws InterruptedException {
        TimerTask task = new TimerTask() {
            @Override
            public void run() {
                log.debug("當前時間{}執行緒名稱{}", DateTime.now(),
                        Thread.currentThread().getName());
            }
        };

        log.debug("當前時間{}執行緒名稱{}", DateTime.now(),
                Thread.currentThread().getName());
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3);
        executorService.scheduleAtFixedRate(task, 1000L,1000L, TimeUnit.MILLISECONDS);
        Thread.sleep(1000+1000*4);
        executorService.shutdown();
    }
}

輸出結果如下所示:

14:43:41.740 [main] DEBUG top.springtask.ScheduledThreadPoolExecutorDemo - 當前時間2022-04-27 14:43:41執行緒名稱main
14:43:42.752 [pool-1-thread-1] DEBUG top.springtask.ScheduledThreadPoolExecutorDemo - 當前時間2022-04-27 14:43:42執行緒名稱pool-1-thread-1
14:43:43.748 [pool-1-thread-1] DEBUG top.springtask.ScheduledThreadPoolExecutorDemo - 當前時間2022-04-27 14:43:43執行緒名稱pool-1-thread-1
14:43:44.749 [pool-1-thread-2] DEBUG top.springtask.ScheduledThreadPoolExecutorDemo - 當前時間2022-04-27 14:43:44執行緒名稱pool-1-thread-2
14:43:45.749 [pool-1-thread-2] DEBUG top.springtask.ScheduledThreadPoolExecutorDemo - 當前時間2022-04-27 14:43:45執行緒名稱pool-1-thread-2
14:43:46.749 [pool-1-thread-2] DEBUG top.springtask.ScheduledThreadPoolExecutorDemo - 當前時間2022-04-27 14:43:46執行緒名稱pool-1-thread-2

Spring Task

Spring Task 是 Spring 提供的輕量級定時任務工具,也就意味著不需要再新增第三方依賴了,相比其他第三方類庫更加方便易用。

好像關於 Spring Task,沒有其他廢話可說了,我們來直接上手。

第一步,新建配置類 SpringTaskConfig,並新增 @EnableScheduling註解開啟 Spring Task。

@Configuration
@EnableScheduling
public class SpringTaskConfig {
}

當然了,也可以不新建這個配置類,直接在主類上新增 @EnableScheduling 註解。

@SpringBootApplication
@EnableScheduling
public class CodingmoreSpringtaskApplication {

	public static void main(String[] args) {
		SpringApplication.run(CodingmoreSpringtaskApplication.class, args);
	}

}

第二步,新建定時任務類 CronTask,使用 @Scheduled 註解註冊 Cron 表示式執行定時任務。

@Slf4j
@Component
public class CronTask {
    @Scheduled(cron = "0/1 * * ? * ?")
    public void cron() {
        log.info("定時執行,時間{}", DateUtil.now());
    }
}

啟動伺服器端,發現每隔一秒鐘會列印一次日誌,證明 Spring Task 的 cron 表示式形式已經起效了。

預設情況下,@Scheduled 建立的執行緒池大小為 1,如果想增加執行緒池大小的話,可以讓 SpringTaskConfig 類實現 SchedulingConfigurer 介面,通過 setPoolSize 增加執行緒池大小。

@Configuration
@EnableScheduling
public class SpringTaskConfig implements SchedulingConfigurer {
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();

        threadPoolTaskScheduler.setPoolSize(10);
        threadPoolTaskScheduler.setThreadNamePrefix("my-scheduled-task-pool-");
        threadPoolTaskScheduler.initialize();

        taskRegistrar.setTaskScheduler(threadPoolTaskScheduler);
    }
}

服務熱部署完成後,會在控制檯看到這樣的資訊:

可以確認自定義執行緒池大小已經生效了,有的任務用的是執行緒led-task-pool-3,有的是執行緒led-task-pool-7,跑時間長了,可以發現 led-task-pool-1 到 led-task-pool-10 的都有。

Spring Task 除了支援 Cron 表示式,還有 fixedRate(固定速率執行)、fixedDelay(固定延遲執行)、initialDelay(初始延遲)三種用法。

/**
 * fixedRate:固定速率執行。每5秒執行一次。
 */
@Scheduled(fixedRate = 5000)
public void reportCurrentTimeWithFixedRate() {
    log.info("Current Thread : {}", Thread.currentThread().getName());
    log.info("Fixed Rate Task : The time is now {}", DateUtil.now());
}

/**
 * fixedDelay:固定延遲執行。距離上一次呼叫成功後2秒才執。
 */
@Scheduled(fixedDelay = 2000)
public void reportCurrentTimeWithFixedDelay() {
    try {
        TimeUnit.SECONDS.sleep(3);
        log.info("Fixed Delay Task : The time is now {}",DateUtil.now());
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

/**
 * initialDelay:初始延遲。任務的第一次執行將延遲5秒,然後將以5秒的固定間隔執行。
 */
@Scheduled(initialDelay = 5000, fixedRate = 5000)
public void reportCurrentTimeWithInitialDelay() {
    log.info("Fixed Rate Task with Initial Delay : The time is now {}", DateUtil.now());
}

不過,fixedRate 有個坑,假如某個方法的定時器設定的固定速率是每5秒執行一次,這個方法現在要執行下面四個任務,四個任務的耗時是:6s、6s、 2s、 3s,任務會如何執行呢(單執行緒環境下)?

2022-04-27 15:25:52.400  INFO 4343 --- [led-task-pool-1] c.codingmore.component.PublishPostTask   : Fixed Rate Task : The time is now 2022-04-27 15:25:52
2022-04-27 15:25:58.401  INFO 4343 --- [led-task-pool-1] c.codingmore.component.PublishPostTask   : Fixed Rate Task : The time is now 2022-04-27 15:25:58
2022-04-27 15:26:00.407  INFO 4343 --- [led-task-pool-1] c.codingmore.component.PublishPostTask   : Fixed Rate Task : The time is now 2022-04-27 15:26:00
2022-04-27 15:26:04.318  INFO 4343 --- [led-task-pool-1] c.codingmore.component.PublishPostTask   : Fixed Rate Task : The time is now 2022-04-27 15:26:04

第一個任務開始的相對時間是第 0 秒,但由於執行了 6 秒,所以原來應該是第 5 秒執行的任務,延遲到第 6 秒才開始執行,第三個任務延遲了 12 秒,原本應該是第 10 秒執行,第三個任務沒有延遲,正常 15 秒後執行。

假如我們使用 @EnableAsync 註解開啟多執行緒環境的話,結果會怎麼樣呢?

2022-04-27 15:33:01.385  INFO 4421 --- [led-task-pool-1] c.codingmore.component.PublishPostTask   : Fixed Rate Task : The time is now 2022-04-27 15:33:01
2022-04-27 15:33:07.390  INFO 4421 --- [led-task-pool-1] c.codingmore.component.PublishPostTask   : Fixed Rate Task : The time is now 2022-04-27 15:33:07
2022-04-27 15:33:09.391  INFO 4421 --- [led-task-pool-1] c.codingmore.component.PublishPostTask   : Fixed Rate Task : The time is now 2022-04-27 15:33:09
2022-04-27 15:33:13.295  INFO 4421 --- [led-task-pool-1] c.codingmore.component.PublishPostTask   : Fixed Rate Task : The time is now 2022-04-27 15:33:13

關於 Cron 表示式

這裡順帶普及一下 Cron 表示式,在定時任務中會經常會遇到。Cron 這個詞來源於希臘語 chronos,原意也就是時間。

Cron 表示式是一個含有時間意義的字串,以 5 個空格隔開,分成 6 個時間元素。舉幾個例子就一目瞭然了。

示例 說明
0 15 10 ? * * 每天上午10:15執行任務
0 0 10,14,16 * * ? 每天10 點、14 點、16 點執行任務
0 0 12 ? * 3 每個星期三中午 12 點執行任務
0 15 10 15 * ? 每月 15 日上午 10 點 15 執行任務

Cron 的語法格式可以總結為:

Seconds Minutes Hours DayofMonth Month DayofWeek

每個時間元素的取值範圍,以及可出現的特殊字元如下所示。

時間元素 取值範圍 可出現的特殊字元
[0,59] *,-/
分鐘 [0,59] *,-/
小時 [0,59] *,-/
日期 [0,31] *,-/?LW
月份 [1,12] *,-/
星期 [1,7] *,-/?L#

特殊字元的含義和示例如下所示。

特殊字元 含義 示例
* 所有可能的值 很好理解,月域中為每個月,星期域中每個星期幾
, 列舉的值 很好理解,小時域中 10,14,16,就表示這幾個小時可選
- 範圍 很好理解,分鐘域中 10-19,就表示 10-19 分鐘每隔一分鐘執行一次
/ 指定數值的增量 很好理解,分鐘域中 0/15,就表示每隔 15 分鐘執行一次
? 不指定值 很好理解,日期域指定了星期域就不能指定值,反之亦然,因為日期域和星期域屬於衝突關係
L 單詞 Last 的首字母 很好理解,日期域和星期域支援,表示月的最後一天或者星期的最後一天
W 除週末以外的工作日 很好理解,僅日期域支援
# 每個月的第幾個星期幾 很好理解,僅星期域支援,4#2表示某月的第二個星期四

關於 Quartz

Quartz 是一款功能強大的開源的任務排程框架,在 GitHub 上已經累計有 5k+ 的 star 了。小到單機應用,大到分散式,都可以整合 Quartz。

在使用 Quartz 之前,讓我們先來搞清楚 4 個核心概念:

  • Job:任務,要執行的具體內容。
  • JobDetail:任務詳情,Job 是它要執行的內容,同時包含了這個任務排程的策略和方案。
  • Trigger:觸發器,可以通過 Cron 表示式來指定任務執行的時間。
  • Scheduler:排程器,可以註冊多個 JobDetail 和 Trigger,用來排程、暫停和刪除任務。

整合 Quartz

Quartz 儲存任務的方式有兩種,一種是使用記憶體,另外一種是使用資料庫。記憶體在程式重啟後就丟失了,所以我們這次使用資料庫的方式來進行任務的持久化。

第一步,在 pom.xml 檔案中新增 Quartz 的 starter。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
    <version>2.6.7</version>
</dependency>

第二步,在 application.yml 新增 Quartz 相關配置,配置說明直接看註釋。

spring:
  quartz:
    job-store-type: jdbc # 預設為記憶體 memory 的方式,這裡我們使用資料庫的形式
    wait-for-jobs-to-complete-on-shutdown: true # 關閉時等待任務完成
    overwrite-existing-jobs: true # 可以覆蓋已有的任務
    jdbc:
      initialize-schema: never # 是否自動使用 SQL 初始化 Quartz 表結構
    properties: # quartz原生配置
      org:
        quartz:
          scheduler:
            instanceName: scheduler # 排程器例項名稱
            instanceId: AUTO # 排程器例項ID自動生成
          # JobStore 相關配置
          jobStore:
            class: org.quartz.impl.jdbcjobstore.JobStoreTX # JobStore 實現類
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate # 使用完全相容JDBC的驅動
            tablePrefix: QRTZ_ # Quartz 表字首
            useProperties: false # 是否將JobDataMap中的屬性轉為字串儲存
          # 執行緒池相關配置
          threadPool:
            threadCount: 25 # 執行緒池大小。預設為 10 。
            threadPriority: 5 # 執行緒優先順序
            class: org.quartz.simpl.SimpleThreadPool # 指定執行緒池實現類,對排程器提供固定大小的執行緒池

Quartz 預設使用的是記憶體的方式來儲存任務,為了持久化,我們這裡改為 JDBC 的形式,並且指定 spring.quartz.jdbc.initialize-schema=never,這樣我們可以手動建立資料表。因為該值的另外兩個選項ALWAYS和EMBEDDED都不太符合我們的要求:

  • ALWAYS:每次都初始化
  • EMBEDDED:只初始化嵌入式資料庫,比如說 H2、HSQL

那手動建立資料表的 SQL 語句去哪裡找呢?

GitHub 地址:https://github.com/quartz-scheduler/quartz/tree/master/quartz-core/src/main/resources/org/quartz/impl/jdbcjobstore

為了方便小夥伴們下載,我把它放在了本教程的原始碼裡面了:

如果使用 Intellij IDEA 旗艦版的話,首次開啟 SQL 檔案的時候會提示你指定資料來源。在上圖中,我配置了本地的 MySQL 資料庫,匯入成功後可以在資料庫中檢視到以下資料表:

Quartz資料庫核心表如下:

Table Name Description
QRTZ_CALENDARS 儲存Quartz的Calendar資訊
QRTZ_CRON_TRIGGERS 儲存CronTrigger,包括Cron表示式和時區資訊
QRTZ_FIRED_TRIGGERS 儲存與已觸發的Trigger相關的狀態資訊,以及相聯Job的執行資訊
QRTZ_PAUSED_TRIGGER_GRPS 儲存已暫停的Trigger組的資訊
QRTZ_SCHEDULER_STATE 儲存少量的有關Scheduler的狀態資訊,和別的Scheduler例項
QRTZ_LOCKS 儲存程式的悲觀鎖的資訊
QRTZ_JOB_DETAILS 儲存每一個已配置的Job的詳細資訊
QRTZ_JOB_LISTENERS 儲存有關已配置的JobListener的資訊
QRTZ_SIMPLE_TRIGGERS 儲存簡單的Trigger,包括重複次數、間隔、以及已觸的次數
QRTZ_BLOG_TRIGGERS Trigger作為Blob型別儲存
QRTZ_TRIGGER_LISTENERS 儲存已配置的TriggerListener的資訊
QRTZ_TRIGGERS 儲存已配置的Trigger的資訊

剩下的就是對 Quartz 的 scheduler、jobStore 和 threadPool 配置。

第三步,建立任務排程的介面 IScheduleService,定義三個方法,分別是通過 Cron 表示式來排程任務、指定時間來排程任務,以及取消任務。

public interface IScheduleService {
    /**
     * 通過 Cron 表示式來排程任務
     */
    String scheduleJob(Class<? extends Job> jobBeanClass, String cron, String data);

    /**
     * 指定時間來排程任務
     */
    String scheduleFixTimeJob(Class<? extends Job> jobBeanClass, Date startTime, String data);

    /**
     * 取消定時任務
     */
    Boolean cancelScheduleJob(String jobName);
}

第四步,建立任務排程業務實現類 ScheduleServiceImpl,通過Scheduler、CronTrigger、JobDetail的API來實現對應的方法。

@Slf4j
@Service
public class ScheduleServiceImpl implements IScheduleService {
    private String defaultGroup = "default_group";

    @Autowired
    private Scheduler scheduler;
    @Override
    public String scheduleJob(Class<? extends Job> jobBeanClass, String cron, String data) {
        String jobName = UUID.fastUUID().toString();
        JobDetail jobDetail = JobBuilder.newJob(jobBeanClass)
                .withIdentity(jobName, defaultGroup)
                .usingJobData("data", data)
                .build();
        //建立觸發器,指定任務執行時間
        CronTrigger cronTrigger = TriggerBuilder.newTrigger()
                .withIdentity(jobName, defaultGroup)
                .withSchedule(CronScheduleBuilder.cronSchedule(cron))
                .build();
        // 排程器進行任務排程
        try {
            scheduler.scheduleJob(jobDetail, cronTrigger);
        } catch (SchedulerException e) {
            log.error("任務排程執行失敗{}", e.getMessage());
        }
        return jobName;
    }

    @Override
    public String scheduleFixTimeJob(Class<? extends Job> jobBeanClass, Date startTime, String data) {
        //日期轉CRON表示式
        String startCron = String.format("%d %d %d %d %d ? %d",
                DateUtil.second(startTime),
                DateUtil.minute(startTime),
                DateUtil.hour(startTime, true),
                DateUtil.dayOfMonth(startTime),
                DateUtil.month(startTime) + 1,
                DateUtil.year(startTime));
        return scheduleJob(jobBeanClass, startCron, data);
    }

    @Override
    public Boolean cancelScheduleJob(String jobName) {
        boolean success = false;
        try {
            // 暫停觸發器
            scheduler.pauseTrigger(new TriggerKey(jobName, defaultGroup));
            // 移除觸發器中的任務
            scheduler.unscheduleJob(new TriggerKey(jobName, defaultGroup));
            // 刪除任務
            scheduler.deleteJob(new JobKey(jobName, defaultGroup));
            success = true;
        } catch (SchedulerException e) {
            log.error("任務取消失敗{}", e.getMessage());
        }
        return success;
    }
}

第五步,定義好要執行的任務,繼承 QuartzJobBean 類,實現
executeInternal 方法,這裡只定義一個定時釋出文章的任務。

@Slf4j
@Component
public class PublishPostJob extends QuartzJobBean {
    @Autowired
    private IScheduleService scheduleService;
    @Autowired
    private IPostsService postsService;

    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        Trigger trigger = jobExecutionContext.getTrigger();
        JobDetail jobDetail = jobExecutionContext.getJobDetail();
        JobDataMap jobDataMap = jobDetail.getJobDataMap();
        Long data = jobDataMap.getLong("data");
        log.debug("定時釋出文章操作:{}",data);

        // 獲取文章的 ID後獲取文章,更新文章為釋出的狀態,還有釋出的時間
        boolean success = postsService.updatePostByScheduler(data);

        //完成後刪除觸發器和任務
        if (success) {
            log.debug("定時任務執行成功,開始清除定時任務");
            scheduleService.cancelScheduleJob(trigger.getKey().getName());
        }
    }
}

第六步,釋出文章的介面裡 PostsServiceImpl 新增定時釋出的任務排程方法。

@Service
public class PostsServiceImpl extends ServiceImpl<PostsMapper, Posts> implements IPostsService {

    private void handleScheduledAfter(Posts posts) {
        // 文章已經儲存為草稿了,並且拿到了文章 ID
        // 呼叫定時任務
        String jobName = scheduleService.scheduleFixTimeJob(PublishPostJob.class, posts.getPostDate(), posts.getPostsId().toString());
        LOGGER.debug("定時任務{}開始執行", jobName);
    }

}

好,我們現在啟動服務,通過Swagger 來測試一下,注意設定文章的定時釋出時間。

檢視 Quartz 的資料表 qrtz_cron_triggers,發現任務已經新增進來了。

qrtz_job_details 表裡也可以檢視具體的任務詳情。

文章定時釋出的時間到了之後,在日誌裡也可以看到 Quartz 的執行日誌。

再次檢視 Quartz 資料表 qrtz_cron_triggers 和 qrtz_job_details 的時候,也會發現定時任務已經清除了。

整體上來說,Spring Boot 整合 Quartz還是非常絲滑的,配置少,步驟清晰,比 Spring Task 更強大,既能針對記憶體也能持久化,所以大家在遇到定時任務的時候完全可以嘗試一把。

完整的功能在程式設計喵實戰專案中已經實現了,可以把程式設計喵匯入到本地嘗試一下。

業務梳理

簡單來梳理一下程式設計喵定時釋出文章的業務。

1)使用者在釋出文章的時候可以選擇定時釋出,如果選擇定時釋出,那麼就要設定定時釋出的時間,暫時規定至少十分鐘以後可以定時。

2)當管理端使用者選擇了定時釋出,那麼在儲存文章的時候,文章狀態要先設定為草稿狀態,對前端使用者是不可見的狀態。

3)儲存文章的時候通知 Quartz,我有一個任務,你需要在某個規定的時間去執行。

scheduleService.scheduleFixTimeJob(PublishPostJob.class, posts.getPostDate(), posts.getPostsId().toString());

4)Quartz 收到這個通知後,就會在資料庫中寫入任務,具體的任務是到指定時間把文章從草稿的狀態轉為釋出狀態,這時候,前端使用者就可以看得見文章了。

// 獲取文章的 ID後獲取文章,更新文章為釋出的狀態,還有釋出的時間
boolean success = postsService.updatePostByScheduler(data);

同時,將任務清除。

// 暫停觸發器
scheduler.pauseTrigger(new TriggerKey(jobName, defaultGroup));
// 移除觸發器中的任務
scheduler.unscheduleJob(new TriggerKey(jobName, defaultGroup));
// 刪除任務
scheduler.deleteJob(new JobKey(jobName, defaultGroup));

整個過程就完成了。Quartz 是如何實現定時釋出文章的呢?其實也是通過 Cron 表示式。

CronTrigger cronTrigger = TriggerBuilder.newTrigger()
                .withIdentity(jobName, defaultGroup)
                .withSchedule(CronScheduleBuilder.cronSchedule(cron))
                .build();

也就是當我們傳入一個指定時間後,通過計算,計算出 Cron 表示式。

String startCron = String.format("%d %d %d %d %d ? %d",
                DateUtil.second(startTime),
                DateUtil.minute(startTime),
                DateUtil.hour(startTime, true),
                DateUtil.dayOfMonth(startTime),
                DateUtil.month(startTime) + 1,
                DateUtil.year(startTime));

在 Quartz 中,有兩類執行緒:Scheduler排程執行緒和任務執行執行緒。

  • 任務執行執行緒:Quartz不會在主執行緒(QuartzSchedulerThread)中處理使用者的Job。Quartz把執行緒管理的職責委託給ThreadPool,一般的設定使用SimpleThreadPool。SimpleThreadPool建立了一定數量的WorkerThread例項來使得Job能夠線上程中進行處理。WorkerThread是定義在SimpleThreadPool類中的內部類,它實質上就是一個執行緒。
  • QuartzSchedulerThread排程主執行緒:QuartzScheduler被建立時建立一個QuartzSchedulerThread例項。

原始碼路徑

本文已收錄到 GitHub 上星標 2.4k+ 的開源專欄《Java 程式設計師進階之路》,據說每一個優秀的 Java 程式設計師都喜歡她,風趣幽默、通俗易懂。內容包括 Java 基礎、Java 併發程式設計、Java 虛擬機器、Java 企業級開發、Java 面試等核心知識點。學 Java,就認準 Java 程式設計師進階之路?。

https://github.com/itwanger/toBeBetterJavaer

star 了這個倉庫就等於你擁有了成為了一名優秀 Java 工程師的潛力。該開源倉庫最近又上 GitHub trending 榜單了,看來是大家都非常認可呀!

在這裡插入圖片描述

沒有什麼使我停留——除了目的,縱然岸旁有玫瑰、有綠蔭、有寧靜的港灣,我是不繫之舟

相關文章