java週期排程幾種實現

yanjiu_lj發表於2015-09-28
寫程式碼的時候有些任務需要做週期處理,例如每天9點解析檔案並落地到資料庫。這時候就需要選擇合適的任務排程策略。以下是java裡常用的幾個任務排程方法:

1、java.util.Timer, java.util.TimerTask
timer啟動一個非同步執行緒執行任務,週期執行任務策略對應方法scheduleAtFixedRate(TimerTask task, long delay, long period),scheduleAtFixedRate(TimerTask task, Date firstTime, long period)可以指定首次任務時間和週期間隔時間。停止任務執行使用cancel()。
Timer執行任務是單執行緒的,內部用任務佇列來維護待執行任務,任務使用最小堆演算法排序(任務下次執行時間距今越小越優先被執行),新增任務時使用鎖機制防止併發問題。但是如果自定義的任務處理邏輯包含訪問競爭資源,則需要自己進行相關競爭資源的防併發處理。

示例程式碼如下:

        final Timer timer = new Timer();
        final AtomicInteger taskExecuteCnt = new AtomicInteger();

        TimerTask timerTaskA = new TimerTask() {
            @Override
            public void run() {
                // scheduler work content
                if (taskExecuteCnt.get() >= 10) {
                    timer.cancel();
                }

                System.out.printf("Task A execute,current time:%s\n", new Date());
                taskExecuteCnt.incrementAndGet();
            }
        };

        TimerTask timerTaskB = new TimerTask() {
            @Override
            public void run() {
                // scheduler work content
                if (taskExecuteCnt.get() >= 10) {
                    timer.cancel();
                }

                System.out.printf("Task B execute,current time:%s\n", new Date());
                taskExecuteCnt.incrementAndGet();
            }
        };

        timer.scheduleAtFixedRate(timerTaskA, 0, 500);
        timer.scheduleAtFixedRate(timerTaskB, 0, 500);

執行後結果:
Task A execute,current time:Mon Sep 28 14:08:21 CST 2015
Task B execute,current time:Mon Sep 28 14:08:21 CST 2015
Task B execute,current time:Mon Sep 28 14:08:22 CST 2015
Task A execute,current time:Mon Sep 28 14:08:22 CST 2015
Task A execute,current time:Mon Sep 28 14:08:22 CST 2015
Task B execute,current time:Mon Sep 28 14:08:22 CST 2015
Task B execute,current time:Mon Sep 28 14:08:23 CST 2015
Task A execute,current time:Mon Sep 28 14:08:23 CST 2015
Task A execute,current time:Mon Sep 28 14:08:23 CST 2015
Task B execute,current time:Mon Sep 28 14:08:23 CST 2015
Task B execute,current time:Mon Sep 28 14:08:24 CST 2015

如上,執行順序為A,B,B,A,A,B,B,A,A,B,B。出現這種同一個任務連續執行是因為Timer內部用佇列維護待處理任務,佇列排序演算法為最小二叉堆(任務下次執行時間距今越小越優先被執行),上述A,B任務的起始執行時間都為0,週期間隔都為500ms。那麼待執行任務佇列堆如下
       A
   B    A1
B1

A執行完後,經過重排堆變為

        B
   B1   A1

B執行完後,經過重排堆變為

        B1
             A1
所以就會出現堆重排序導致優先順序相等的任務執行順序不固定,如果需要多個任務以特定順序執行,可通過指定任務首次執行延遲時間來實現,
        timer.scheduleAtFixedRate(timerTaskA, 0, 500);
        timer.scheduleAtFixedRate(timerTaskB, 1, 500);
如上指定,則同一個週期內A任務永遠先與B任務執行。

另外,如果某個任務執行時間過長,導致下次觸發任務執行時佇列中多個任務下次執行時間已到,則會發生多個任務連續執行。如果任務執行時間對執行結果有影響,則合理指定任務週期,單任務處理的資料量,或者使用非同步處理機制來確保任務執行正確性(任務觸發時只記錄一條任務流水,流水記錄中包括任務觸發時的狀態,後續通過額外的恢復任務處理任務流水,非同步處理邏輯還可以保證在系統故障時不會丟失佇列中待處理的任務)。


2、java.util.concurrent .ScheduledExecutorService

使用java.util.concurrent .ScheduledExecutorService執行任務時,ScheduledExecutorService內部使用執行緒池執行任務,可以指定執行任務的執行緒池大小。併發任務較多時可以配置合適的執行緒池大小提高任務執行效率。


示例程式碼如下:
        final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
        final AtomicInteger taskExecuteCnt = new AtomicInteger();

        Runnable runnableA = new Runnable() {
            @Override
            public void run() {
                // scheduler work content
                if (taskExecuteCnt.get() >= 10) {
                    scheduledExecutorService.shutdownNow();
                }

                System.out.printf("Task A execute,current time:%s\n", new Date());
                taskExecuteCnt.incrementAndGet();
            }
        };

        Runnable runnableB = new Runnable() {
            @Override
            public void run() {
                // scheduler work content
                if (taskExecuteCnt.get() >= 10) {
                    scheduledExecutorService.shutdownNow();
                }

                System.out.printf("Task B execute,current time:%s\n", new Date());
                taskExecuteCnt.incrementAndGet();
            }
        };

        scheduledExecutorService.scheduleWithFixedDelay(runnableA, 0, 500, TimeUnit.MILLISECONDS);
        scheduledExecutorService.scheduleWithFixedDelay(runnableB, 0, 500,TimeUnit.MILLISECONDS);

這裡使用的排程方法是scheduleWithFixedDelay,和scheduleWithFixedRate的區別是:

scheduleWithFixedDelay任務的下次執行時間是根據前次執行結束時間和時間間隔計算得到。
scheduleWithFixedRate任務的下次執行時間是根據前次執行開始時間加時間間隔得到。

使用兩個方法可能帶來的影響是,如果任務執行時間過長,scheduleWithFixedDelay計算得到的待執行任務個數小於scheduleWithFixedRate計算得到的任務個數。


3、Quartz
Quzrtz支援用cron表示式來定義靈活多變的任務觸發時間,例如表示式(0/1 * * * * ?)表示每秒觸發任務執行。

spring quarz任務排程示例程式碼如下:

配置需要的beans,配置檔案為quartz.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"
    default-autowire="byName">

    <bean id="cronTaskA" class="com.test.CronTask">
        <property name="jobContent" value="Task A execute" />
    </bean>
    <bean id="cronTaskB" class="com.test.CronTask">
        <property name="jobContent" value="Task B execute" />
    </bean>

    <bean id="jobDetailA"
        class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
        <property name="targetObject" ref="cronTaskA" />
        <property name="targetMethod" value="run" />
        <property name="concurrent" value="false" />
    </bean>

    <bean id="jobDetailB"
        class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
        <property name="targetObject" ref="cronTaskB" />
        <property name="targetMethod" value="run" />
        <property name="concurrent" value="false" />
    </bean>

    <bean id="cronTriggerA" class="org.springframework.scheduling.quartz.CronTriggerBean">
        <!-- jobDetail -->
        <property name="jobDetail" ref="jobDetailA" />

        <!-- execute per second -->
        <property name="cronExpression" value="0/1 * * * * ?" />
    </bean>

    <bean id="cronTriggerB" class="org.springframework.scheduling.quartz.CronTriggerBean">
        <!-- jobDetail -->
        <property name="jobDetail" ref="jobDetailB" />

        <!-- execute per second -->
        <property name="cronExpression" value="0/1 * * * * ?" />
    </bean>

    <bean id="schedulerFactory"
        class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
        <property name="triggers">
            <list>
                <ref bean="cronTriggerA" />
                <ref bean="cronTriggerB" />
            </list>
        </property>
    </bean>
</beans>

自定義任務類實現:
public class CronTask {

    /** logger */
    private static final Logger logger = LoggerFactory.getLogger(CronTask.class);

    /** job content */
    private String              jobContent;

    /**
     * task content
     */
    public void run() {
        if (logger.isInfoEnabled()) {
            logger.info(jobContent);
        }
    }

    /**
     * Setter method for property <tt>jobContent</tt>.
     *
     * @param jobContent value to be assigned to property jobContent
     */
    public void setJobContent(String jobContent) {
        this.jobContent = jobContent;
    }

}


任務排程測試程式碼:

        try {
            String xmlPath = System.getProperty("user.dir") +File.separator + "quartz.xml";

            System.out.println(FileUtils.readLines(new File(xmlPath)));

            new FileSystemXmlApplicationContext(xmlPath);

            logger.info("Run QuartzJob");

        } catch (Exception e) {
            logger.error("Exception occurred during quartz job execution.", e);
        }

相關文章