現在有一個很棘手的問題:客戶要贈加一個功能,就是每天晚上11點要統計一下數據,并存到一個文件中,我試著用線程,但是總達不到理想的效果。請給點思路,多謝了。
我們的開發環境是tomcat和servlet,我是這樣處理的,在啟動tomcat時就開一個線程來檢測時間并判斷睡眠多長時間, 還有沒有其他的方式?真的沒思路了。請各位指點指點吧!
==================================
如何在Web工程中實現任務計劃調度,好多朋友用過Windows的任務計劃
經過查閱較多相關資料,發現Java定時器(java.util.Timer)有定時觸發計劃任務的功能,通過配置定時器的間隔時間,在某一間隔時間段之后會自動有規律的調用預先所安排的計劃任務(java.util.TimerTask)。另外,由于我們希望當Web工程啟動時,定時器能自動開始計時,在整個Web工程的生命期里,定時器能在每晚深夜觸發一次報表計算引擎。因此定時器的存放位置也值得考查,不能簡單的存在于單個Servlet或JavaBean中,必須能讓定時器宿主的存活期為整個Web工程生命期,在工程啟動時能自動加載運行。結合這兩點,跟Servlet上下文有關的偵聽器就最合適不過了,通過在工程的配置文件中加以合理配置,會在工程啟動時自動運行,并在整個工程生命期中處于監聽狀態。
下面就Servlet偵聽器結合Java定時器來講述整個實現過程。要運用Servlet偵聽器需要實現javax.servlet.ServletContextListener接口,同時實現它的contextInitialized(ServletContextEvent event)和contextDestroyed(ServletContextEvent event)兩個接口函數。考慮定時器有個建立和銷毀的過程,看了前面兩個接口函數,就不容置疑的把建立的過程置入contextInitialized,把銷毀的過程置入contextDestroyed了。
我把ServletContextListener的實現類取名為ContextListener,在其內添加一個定時器,示例代碼如下所示(為考慮篇幅,僅提供部分代碼供讀者參考):
1. private java.util.Timer timer = null;
2. public void contextInitialized(ServletContextEvent event) {
3. timer = new java.util.Timer(true);
4. event.getServletContext().log("定時器已啟動");
5. timer.schedule(new MyTask(event.getServletContext()), 0, 60*60*1000);
6. event.getServletContext().log("已經添加任務調度表");
7. }
8. public void contextDestroyed(ServletContextEvent event) {
9. timer.cancel();
10. event.getServletContext().log("定時器銷毀");
11. }
以上代碼中, timer.schedule(new MyTask(event.getServletContext()), 0, 60*60*1000)這一行為定時器調度語句,其中MyTask是自定義需要被調度的執行任務(在我的財政數據中心項目中就是報表計算引擎入口),從java.util.TimerTask繼承,下面會重點講述,第三個參數表示每小時(即60*60*1000毫秒)被觸發一次,中間參數0表示無延遲。其它代碼相當簡單,不再詳細說明。
下面介紹MyTask的實現,上面的代碼中看到了在構造MyTask時,傳入了javax.servlet.ServletContext類型參數,是為記錄Servlet日志方便而傳入,因此需要重載MyTask的構造函數(其父類java.util.TimerTask原構造函數是沒有參數的)。在timer.schedule()的調度中,設置了每小時調度一次,因此如果想實現調度任務每24小時被執行一次,還需要判斷一下時鐘點,以常量C_SCHEDULE_HOUR表示(晚上12點,也即0點)。同時為防止24小時執行下來,任務還未執行完(當然,一般任務是沒有這么長的),避免第二次又被調度以引起執行沖突,設置了當前是否正在執行的狀態標志isRunning。示例代碼如下所示:
1. private static final int C_SCHEDULE_HOUR = 0;
2. private static boolean isRunning = false;
3. private ServletContext context = null;
4. public MyTask(ServletContext context) {
5. this.context = context;
6. }
7. public void run() {
8. Calendar cal = Calendar.getInstance();
9. if (!isRunning) {
10. if (C_SCHEDULE_HOUR == cal.get(Calendar.HOUR_OF_DAY)) {
11. isRunning = true;
12. context.log("開始執行指定任務");
13.
14. //TODO 添加自定義的詳細任務,以下只是示例
15. int i = 0;
16. while (i++ < 10) {
17. context.log("已完成任務的" + i + "/" + 10);
18. }
19.
20. isRunning = false;
21. context.log("指定任務執行結束");
22. }
23. } else {
24. context.log("上一次任務執行還未結束");
25. }
26. }
上面代碼中“//TODO……”之下四行是真正被調度執行的演示代碼(在我的財政數據中心項目中就是報表計算過程),您可以換成自己希望執行的語句。
到這兒,ServletContextListener和MyTask的代碼都已完整了。最后一步就是把ServletContextListener部署到您的Web工程中去,在您工程的web.xml配置文件中加入如下三行:
<listener>
<listener-class>com.test.ContextListener</listener-class>
</listener>
當然,上面的com.test得換成您自己的包名了。保存web.xml文件后,把工程打包部署到Tomcat中即可。任務會在每晚12點至凌晨1點之間被執行,上面的代碼會在Tomcat的日志文件中記錄如下:
2003-12-05 0:21:39 開始執行指定任務
2003-12-05 0:21:39 已完成任務的1/10
……
2003-12-05 0:21:39 已完成任務的10/10
2003-12-05 0:21:39 指定任務執行結束
Feedback
# re: Q : 如何實現每天定時對數據庫的操作 回復 更多評論
2006-01-22 22:37 by caid'weblog
http://www2.uuzone.com/blog/seril/73267.htm
使用Timmer使Struts修改struts-config.xml文件不用重新啟動服務器 1
在做struts應用的時候,經常學要修改struts-config.xml文件,在每次修改完之后只有重新啟動服務器才能讓修改生效。因此做了一個Listener,在應用啟動的時候開始,每隔一段時間就去檢查一下struts-config.xml文件的最后修改時間,如果修改時間變化了,就重新讀取struts-config.xml,將對應的配置放到ServletContext中去。
一.LoadResourceListener
import java.util.Timer;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import kick.utils.Constants;
public class LoadResourceListener implements ServletContextListener {
/* (non-Javadoc)
* @see javax.servlet.ServletContextListener#contextInitialized(javax.servlet.ServletContextEvent)
*/
public void contextInitialized(ServletContextEvent event) {
Timer loadResource = new Timer();
//獲取ServletContext
ServletContext servletContext = event.getServletContext();
//創建一個LoadResourceTimerTask 的實例
LoadResourceTimerTask loadResourceTimerTask = new LoadResourceTimerTask(servletContext);
//將剛創建的TimerTask的實例的運行計劃訂為:馬上開始,每隔20×1000ms運行一次
loadResource.schedule(loadResourceTimerTask,0,Constants.DELAY_UPDATE_TIME);
}
/* (non-Javadoc)
* @see javax.servlet.ServletContextListener#contextDestroyed(javax.servlet.ServletContextEvent)
*/
public void contextDestroyed(ServletContextEvent arg0) {
}
}
二.配置LoadResourceListener
在filter的配置下面添加上如下的配置
<listener>
<listener-class>kick.load.resource.LoadResourceListener</listener-class>
</listener>
三.LoadResourceTimerTask類
/*
* Created on 2005-9-6
*
* TODO To change the template for this generated file go to
* Window - Preferences - Java - Code Style - Code Templates
*/
package kick.load.resource;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.MissingResourceException;
import java.util.Set;
import java.util.TimerTask;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.UnavailableException;
import javax.sql.DataSource;
import kick.utils.Constants;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.collections.FastHashMap;
import org.apache.commons.digester.Digester;
import org.apache.commons.digester.RuleSet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts.Globals;
import org.apache.struts.config.ConfigRuleSet;
import org.apache.struts.config.DataSourceConfig;
import org.apache.struts.config.FormBeanConfig;
import org.apache.struts.config.MessageResourcesConfig;
import org.apache.struts.config.ModuleConfig;
import org.apache.struts.config.ModuleConfigFactory;
import org.apache.struts.util.MessageResources;
import org.apache.struts.util.MessageResourcesFactory;
import org.apache.struts.util.RequestUtils;
import org.apache.struts.util.ServletContextWriter;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
public class LoadResourceTimerTask extends TimerTask {
private ServletContext context = null;
/**
* <p>
* The resources object for our internal resources.
* </p>
*/
protected MessageResources internal = null;
/**
* <p>
* The Digester used to produce ModuleConfig objects from a Struts
* configuration file.
* </p>
*
* @since Struts 1.1
*/
protected Digester configDigester = null;
/**
* <p>
* The Java base name of our internal resources.
* </p>
*
* @since Struts 1.1
*/
protected String internalName = "org.apache.struts.action.ActionResources";
/**
* <p>
* Commons Logging instance.
* </p>
*
* @since Struts 1.1
*/
protected static Log log = LogFactory.getLog(LoadResourceTimerTask.class);
private List initParams = null;
/**
* <p>
* The set of public identifiers, and corresponding resource names, for the
* versions of the configuration file DTDs that we know about. There
* <strong>MUST </strong> be an even number of Strings in this list!
* </p>
*/
protected String registrations[] = { "-//Apache Software Foundation//DTD Struts Configuration 1.0//EN",
"/org/apache/struts/resources/struts-config_1_0.dtd",
"-//Apache Software Foundation//DTD Struts Configuration 1.1//EN",
"/org/apache/struts/resources/struts-config_1_1.dtd",
"-//Apache Software Foundation//DTD Struts Configuration 1.2//EN",
"/org/apache/struts/resources/struts-config_1_2.dtd",
"-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN", "/org/apache/struts/resources/web-app_2_2.dtd",
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN", "/org/apache/struts/resources/web-app_2_3.dtd" };
/**
* <p>
* The JDBC data sources that has been configured for this module, if any,
* keyed by the servlet context attribute under which they are stored.
* </p>
*/
protected FastHashMap dataSources = new FastHashMap();
/**
* <p>
* Comma-separated list of context-relative path(s) to our configuration
* resource(s) for the default module.
* </p>
*/
protected String config = "/WEB-INF/struts-config.xml";
private List resourcesName = new ArrayList();
private Set resourceFiles = new HashSet();
public LoadResourceTimerTask(ServletContext context) {
this.context = context;
try {
initInternal();
parseWeb();
parseConfigFile();
parseResource();
// parseConfigFile();
} catch (ServletException e) {
System.out.println(e.getMessage());
e.printStackTrace();
throw new RuntimeException(e);
}
}
/*
* (non-Javadoc)
*
* @see java.util.TimerTask#run()
*/
public void run() {
try {
reLoadConfigFile();
reLoadResource();
} catch (Exception e) {
}
}
/**
*
*/
private void parseConfigFile() {
InitParam initParam = null;
for (int i = 0; i < initParams.size(); i++) {
initParam = (InitParam) initParams.get(i);
String name = initParam.getName();
if (!name.startsWith("config")) {
continue;
}
String prefix = name.substring(6);
String paths = initParam.getValue();
// Process each specified resource path
while (paths.length() > 0) {
// digester.push(config);
String path = null;
int comma = paths.indexOf(',');
if (comma >= 0) {
path = paths.substring(0, comma).trim();
paths = paths.substring(comma + 1);
} else {
path = paths.trim();
paths = "";
}
if (path.length() < 1) {
break;
}
File file = new File(getServletContext().getRealPath(path));
StrutsConfig s = new StrutsConfig();
Digester d = new Digester();
d.push(s);
d.setNamespaceAware(false);
d.setValidating(false);
for (int j = 0; j < registrations.length; j += 2) {
URL url = this.getClass().getResource(registrations[j + 1]);
if (url != null) {
d.register(registrations[j], url.toString());
}
}
d.addObjectCreate("struts-config/message-resources", Resource.class);
d.addSetProperties("struts-config/message-resources", "parameter", "parameter");
d.addSetNext("struts-config/message-resources", "addResource");
// d.addCallMethod("web-struts-config/message-resources",
// "setParameter", 0);
try {
d.parse(file);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SAXException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// String resourcePath =
// ((Resource)s.getResources()).toFilePaht();
List rs = s.getResources();
for (int ii = 0; ii < rs.size(); ii++) {
resourcesName.add(((Resource) rs.get(ii)).toFilePaht());
}
}
}
}
public void parseResource() {
int index = 0;
String dirName = "";
String resource = "";
String subFileName = "";
for (int i = 0; i < resourcesName.size(); i++) {
resource = (String) resourcesName.get(i);
index = resource.lastIndexOf("/");
dirName = resource.substring(0, index);
subFileName = resource.substring(index + 1, resource.length());
File file = new File(getServletContext().getRealPath("/WEB-INF/classes/" + dirName));
if (file.isDirectory()) {
String[] fileNames = file.list();
if (fileNames != null) {
for (int j = 0; j < fileNames.length; j++) {
if (fileNames[j] != null) {
if (fileNames[j].trim().startsWith(subFileName.trim())) {
resourceFiles.add(dirName + "/" + fileNames[j]);
// System.out.println("The file Name : '" +
// subFileName + "'");
// System.out.println("Add file name : '" +
// fileNames[j] + "'");
}
}
}
}
}
}
}
private void reLoadConfigFile() {
try {
InitParam initParam = null;
for (int i = 0; i < initParams.size(); i++) {
initParam = (InitParam) initParams.get(i);
String name = initParam.getName();
if (!name.startsWith("config")) {
continue;
}
String prefix = name.substring(6);
String paths = initParam.getValue();
// Process each specified resource path
while (paths.length() > 0) {
// digester.push(config);
String path = null;
int comma = paths.indexOf(',');
if (comma >= 0) {
path = paths.substring(0, comma).trim();
paths = paths.substring(comma + 1);
} else {
path = paths.trim();
paths = "";
}
if (path.length() < 1) {
break;
}
File file = new File(getServletContext().getRealPath(path));
if ((System.currentTimeMillis() - file.lastModified()) < Constants.DELAY_UPDATE_TIME) {
log.debug("The struts-config.xml is changed,will be reload into context.");
log.debug("The file name is : '" + path + "'");
log.debug("Refash the resource file config list!");
parseConfigFile();
log.debug("Refash the resource file list!");
parseResource();
ModuleConfig moduleConfig = initModuleConfig(prefix, path);
initModuleMessageResources(moduleConfig);
initModuleDataSources(moduleConfig);
moduleConfig.freeze();
log.debug("Reload the config file success!");
}
this.initModulePrefixes(this.getServletContext());
}
}
} catch (Exception e) {
}
}
private void reLoadConfig(String prefix, String path) {
try {
ModuleConfig moduleConfig = initModuleConfig(prefix, path);
initModuleMessageResources(moduleConfig);
initModuleDataSources(moduleConfig);
moduleConfig.freeze();
} catch (Exception e) {
}
}
private void reLoadResource() {
Iterator it = resourceFiles.iterator();
String fileName = "";
while (it.hasNext()) {
fileName = (String) it.next();
File file = new File(this.getServletContext().getRealPath("/WEB-INF/classes/" + fileName));
if ((System.currentTimeMillis() - file.lastModified()) < Constants.DELAY_UPDATE_TIME) {
log.debug("Update the '" + file.getName() + "' property file!");
updateConfigFile();
}
}
}
/**
*
*/
private void updateConfigFile() {
InitParam initParam = null;
for (int i = 0; i < initParams.size(); i++) {
initParam = (InitParam) initParams.get(i);
String name = initParam.getName();
if (!name.startsWith("config")) {
continue;
}
String prefix = name.substring(6);
String paths = initParam.getValue();
// Process each specified resource path
while (paths.length() > 0) {
// digester.push(config);
String path = null;
int comma = paths.indexOf(',');
if (comma >= 0) {
path = paths.substring(0, comma).trim();
paths = paths.substring(comma + 1);
} else {
path = paths.trim();
paths = "";
}
if (path.length() < 1) {
break;
}
File file = new File(getServletContext().getRealPath(path));
file.setLastModified(System.currentTimeMillis());
}
}
}
# Java語言中Timer類的簡潔用法 回復 更多評論
2006-01-22 23:01 by caid'weblog
所有類型的 Java 應用程序一般都需要計劃重復執行的任務。企業應用程序需要計劃每日的日志或者晚間批處理過程。一個 J2SE 或者 J2ME 日歷應用程序需要根據用戶的約定計劃鬧鈴時間。不過,標準的調度類 Timer 和 TimerTask 沒有足夠的靈活性,無法支持通常需要的計劃任務類型。在本文中,Java 開發人員 Tom White 向您展示了如何構建一個簡單通用的計劃框架,以用于執行任意復雜的計劃任務。
我將把 java.util.Timer 和 java.util.TimerTask 統稱為 Java 計時器框架,它們使程序員可以很容易地計劃簡單的任務(注意這些類也可用于 J2ME 中)。在 Java 2 SDK, Standard Edition, Version 1.3 中引入這個框架之前,開發人員必須編寫自己的調度程序,這需要花費很大精力來處理線程和復雜的 Object.wait() 方法。不過,Java 計時器框架沒有足夠的能力來滿足許多應用程序的計劃要求。甚至一項需要在每天同一時間重復執行的任務,也不能直接使用 Timer 來計劃,因為在夏令時開始和結束時會出現時間跳躍。
本文展示了一個通用的 Timer 和 TimerTask 計劃框架,從而允許更靈活的計劃任務。這個框架非常簡單 —— 它包括兩個類和一個接口 —— 并且容易掌握。如果您習慣于使用 Java 定時器框架,那么您應該可以很快地掌握這個計劃框架。
計劃單次任務
計劃框架建立在 Java 定時器框架類的基礎之上。因此,在解釋如何使用計劃框架以及如何實現它之前,我們將首先看看如何用這些類進行計劃。
想像一個煮蛋計時器,在數分鐘之后(這時蛋煮好了)它會發出聲音提醒您。清單 1 中的代碼構成了一個簡單的煮蛋計時器的基本結構,它用 Java 語言編寫:
清單 1. EggTimer 類
package org.tiling.scheduling.examples;import java.util.Timer;import java.util.TimerTask;public class EggTimer { private final Timer timer = new Timer(); private final int minutes; public EggTimer(int minutes) { this.minutes = minutes; } public void start() { timer.schedule(new TimerTask() { public void run() { playSound(); timer.cancel(); } private void playSound() { System.out.println("Your egg is ready!"); // Start a new thread to play a sound... } }, minutes * 60 * 1000); } public static void main(String[] args) { EggTimer eggTimer = new EggTimer(2); eggTimer.start(); }}
EggTimer 實例擁有一個 Timer 實例,用于提供必要的計劃。用 start() 方法啟動煮蛋計時器后,它就計劃了一個 TimerTask,在指定的分鐘數之后執行。時間到了,Timer 就在后臺調用 TimerTask 的 start() 方法,這會使它發出聲音。在取消計時器后這個應用程序就會中止。
計劃重復執行的任務
通過指定一個固定的執行頻率或者固定的執行時間間隔,Timer 可以對重復執行的任務進行計劃。不過,有許多應用程序要求更復雜的計劃。例如,每天清晨在同一時間發出叫醒鈴聲的鬧鐘不能簡單地使用固定的計劃頻率 86400000 毫秒(24 小時),因為在鐘撥快或者撥慢(如果您的時區使用夏令時)的那些天里,叫醒可能過晚或者過早。解決方案是使用日歷算法計算每日事件下一次計劃發生的時間。 而這正是計劃框架所支持的。考慮清單 2 中的 AlarmClock 實現:
清單 2. AlarmClock 類
package org.tiling.scheduling.examples;import java.text.SimpleDateFormat;import java.util.Date;import org.tiling.scheduling.Scheduler;import org.tiling.scheduling.SchedulerTask;import org.tiling.scheduling.examples.iterators.DailyIterator;public class AlarmClock { private final Scheduler scheduler = new Scheduler(); private final SimpleDateFormat dateFormat = new SimpleDateFormat("dd MMM yyyy HH:mm:ss.SSS"); private final int hourOfDay, minute, second; public AlarmClock(int hourOfDay, int minute, int second) { this.hourOfDay = hourOfDay; this.minute = minute; this.second = second; } public void start() { scheduler.schedule(new SchedulerTask() { public void run() { soundAlarm(); } private void soundAlarm() { System.out.println("Wake up! " + "It"s " + dateFormat.format(new Date())); // Start a new thread to sound an alarm... } }, new DailyIterator(hourOfDay, minute, second)); } public static void main(String[] args) { AlarmClock alarmClock = new AlarmClock(7, 0, 0); alarmClock.start(); }}
注意這段代碼與煮蛋計時器應用程序非常相似。AlarmClock 實例擁有一個 Scheduler (而不是 Timer)實例,用于提供必要的計劃。啟動后,這個鬧鐘對 SchedulerTask (而不是 TimerTask)進行調度用以發出報警聲。這個鬧鐘不是計劃一個任務在固定的延遲時間后執行,而是用 DailyIterator 類描述其計劃。在這里,它只是計劃任務在每天上午 7:00 執行。下面是一個正常運行情況下的輸出:
Wake up! It"s 24 Aug 2003 07:00:00.023Wake up! It"s 25 Aug 2003 07:00:00.001Wake up! It"s 26 Aug 2003 07:00:00.058Wake up! It"s 27 Aug 2003 07:00:00.015Wake up! It"s 28 Aug 2003 07:00:00.002...
DailyIterator 實現了 ScheduleIterator,這是一個將 SchedulerTask 的計劃執行時間指定為一系列 java.util.Date 對象的接口。然后 next() 方法按時間先后順序迭代 Date 對象。返回值 null 會使任務取消(即它再也不會運行)—— 這樣的話,試圖再次計劃將會拋出一個異常。清單 3 包含 ScheduleIterator 接口:
清單 3. ScheduleIterator 接口
package org.tiling.scheduling;import java.util.Date;public interface ScheduleIterator { public Date next();}
DailyIterator 的 next() 方法返回表示每天同一時間(上午 7:00)的 Date 對象,如清單 4 所示。所以,如果對新構建的 next() 類調用 next(),那么將會得到傳遞給構造函數的那個日期當天或者后面一天的 7:00 AM。再次調用 next() 會返回后一天的 7:00 AM,如此重復。為了實現這種行為,DailyIterator 使用了 java.util.Calendar 實例。構造函數會在日歷中加上一天,對日歷的這種設置使得第一次調用 next() 會返回正確的 Date。注意代碼沒有明確地提到夏令時修正,因為 Calendar 實現(在本例中是 GregorianCalendar)負責對此進行處理,所以不需要這樣做。
清單 4. DailyIterator 類
package org.tiling.scheduling.examples.iterators;import org.tiling.scheduling.ScheduleIterator;import java.util.Calendar;import java.util.Date;/** * A DailyIterator class returns a sequence of dates on subsequent days * representing the same time each day. */public class DailyIterator implements ScheduleIterator { private final int hourOfDay, minute, second; private final Calendar calendar = Calendar.getInstance(); public DailyIterator(int hourOfDay, int minute, int second) { this(hourOfDay, minute, second, new Date()); } public DailyIterator(int hourOfDay, int minute, int second, Date date) { this.hourOfDay = hourOfDay; this.minute = minute; this.second = second; calendar.setTime(date); calendar.set(Calendar.HOUR_OF_DAY, hourOfDay); calendar.set(Calendar.MINUTE, minute); calendar.set(Calendar.SECOND, second); calendar.set(Calendar.MILLISECOND, 0); if (!calendar.getTime().before(date)) { calendar.add(Calendar.DATE, -1); } } public Date next() { calendar.add(Calendar.DATE, 1); return calendar.getTime(); }}
實現計劃框架
在上一節,我們學習了如何使用計劃框架,并將它與 Java 定時器框架進行了比較。下面,我將向您展示如何實現這個框架。除了 清單 3 中展示的 ScheduleIterator 接口,構成這個框架的還有另外兩個類 —— Scheduler 和 SchedulerTask 。這些類實際上在內部使用 Timer 和 SchedulerTask,因為計劃其實就是一系列的單次定時器。清單 5 和 6 顯示了這兩個類的源代碼:
清單 5. Scheduler
package org.tiling.scheduling;import java.util.Date;import java.util.Timer;import java.util.TimerTask;public class Scheduler { class SchedulerTimerTask extends TimerTask { private SchedulerTask schedulerTask; private ScheduleIterator iterator; public SchedulerTimerTask(SchedulerTask schedulerTask, ScheduleIterator iterator) { this.schedulerTask = schedulerTask; this.iterator = iterator; } public void run() { schedulerTask.run(); reschedule(schedulerTask, iterator); } } private final Timer timer = new Timer(); public Scheduler() { } public void cancel() { timer.cancel(); } public void schedule(SchedulerTask schedulerTask, ScheduleIterator iterator) { Date time = iterator.next(); if (time == null) { schedulerTask.cancel(); } else { synchronized(schedulerTask.lock) { if (schedulerTask.state != SchedulerTask.VIRGIN) { throw new IllegalStateException("Task already scheduled " + "or cancelled"); } schedulerTask.state = SchedulerTask.SCHEDULED; schedulerTask.timerTask = new SchedulerTimerTask(schedulerTask, iterator); timer.schedule(schedulerTask.timerTask, time); } } } private void reschedule(SchedulerTask schedulerTask, ScheduleIterator iterator) { Date time = iterator.next(); if (time == null) { schedulerTask.cancel(); } else { synchronized(schedulerTask.lock) { if (schedulerTask.state != SchedulerTask.CANCELLED) { schedulerTask.timerTask = new SchedulerTimerTask(schedulerTask, iterator); timer.schedule(schedulerTask.timerTask, time); } } } }}
清單 6 顯示了 SchedulerTask 類的源代碼:
package org.tiling.scheduling;import java.util.TimerTask;public abstract class SchedulerTask implements Runnable { final Object lock = new Object(); int state = VIRGIN; static final int VIRGIN = 0; static final int SCHEDULED = 1; static final int CANCELLED = 2; TimerTask timerTask; protected SchedulerTask() { } public abstract void run(); public boolean cancel() { synchronized(lock) { if (timerTask != null) { timerTask.cancel(); } boolean result = (state == SCHEDULED); state = CANCELLED; return result; } } public long scheduledExecutionTime() { synchronized(lock) { return timerTask == null ? 0 : timerTask.scheduledExecutionTime(); } }}
就像煮蛋計時器,Scheduler 的每一個實例都擁有 Timer 的一個實例,用于提供底層計劃。Scheduler 并沒有像實現煮蛋計時器時那樣使用一個單次定時器,它將一組單次定時器串接在一起,以便在由 ScheduleIterator 指定的各個時間執行 SchedulerTask 類。
考慮 Scheduler 上的 public schedule() 方法 —— 這是計劃的入口點,因為它是客戶調用的方法(在 取消任務 一節中將描述僅有的另一個 public 方法 cancel())。通過調用 ScheduleIterator 接口的 next(),發現第一次執行 SchedulerTask 的時間。然后通過調用底層 Timer 類的單次 schedule() 方法,啟動計劃在這一時刻執行。為單次執行提供的 TimerTask 對象是嵌入的 SchedulerTimerTask 類的一個實例,它包裝了任務和迭代器(iterator)。在指定的時間,調用嵌入類的 run() 方法,它使用包裝的任務和迭代器引用以便重新計劃任務的下一次執行。reschedule() 方法與 schedule() 方法非常相似,只不過它是 private 的,并且執行一組稍有不同的 SchedulerTask 狀態檢查。重新計劃過程反復重復,為每次計劃執行構造一個新的嵌入類實例,直到任務或者調度程序被取消(或者 JVM 關閉)。
類似于 TimerTask,SchedulerTask 在其生命周期中要經歷一系列的狀態。創建后,它處于 VIRGIN 狀態,這表明它從沒有計劃過。計劃以后,它就變為 SCHEDULED 狀態,再用下面描述的方法之一取消任務后,它就變為 CANCELLED 狀態。管理正確的狀態轉變 —— 如保證不對一個非 VIRGIN 狀態的任務進行兩次計劃 —— 增加了 Scheduler 和 SchedulerTask 類的復雜性。在進行可能改變任務狀態的操作時,代碼必須同步任務的鎖對象。
取消任務
取消計劃任務有三種方式。第一種是調用 SchedulerTask 的 cancel() 方法。這很像調用 TimerTask 的 cancel()方法:任務再也不會運行了,不過已經運行的任務仍會運行完成。 cancel() 方法的返回值是一個布爾值,表示如果沒有調用 cancel() 的話,計劃的任務是否還會運行。更準確地說,如果任務在調用 cancel() 之前是 SCHEDULED 狀態,那么它就返回 true。如果試圖再次計劃一個取消的(甚至是已計劃的)任務,那么 Scheduler 就會拋出一個 IllegalStateException。
取消計劃任務的第二種方式是讓 ScheduleIterator 返回 null。這只是第一種方式的簡化操作,因為 Scheduler 類調用 SchedulerTask 類的 cancel()方法。如果您想用迭代器而不是任務來控制計劃停止時間時,就用得上這種取消任務的方式了。
第三種方式是通過調用其 cancel() 方法取消整個 Scheduler。這會取消調試程序的所有任務,并使它不能再計劃任何任務。
擴展 cron 實用程序
可以將計劃框架比作 UNIX 的 cron 實用程序,只不過計劃次數的規定是強制性而不是聲明性的。例如,在 AlarmClock 實現中使用的 DailyIterator 類,它的計劃與 cron 作業的計劃相同,都是由以 0 7 * * * 開始的 crontab 項指定的(這些字段分別指定分鐘、小時、日、月和星期)。
不過,計劃框架比 cron 更靈活。想像一個在早晨打開熱水的 HeatingController 應用程序。我想指示它“在每個工作日上午 8:00 打開熱水,在周未上午 9:00 打開熱水”。使用 cron,我需要兩個 crontab 項(0 8 * * 1,2,3,4,5 和 0 9 * * 6,7)。而使用 ScheduleIterator 的解決方案更簡潔一些,因為我可以使用復合(composition)來定義單一迭代器。清單 7 顯示了其中的一種方法:
清單 7. 用復合定義單一迭代器
int[] weekdays = new int[] { Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY }; int[] weekend = new int[] { Calendar.SATURDAY, Calendar.SUNDAY }; ScheduleIterator i = new CompositeIterator( new ScheduleIterator[] { new RestrictedDailyIterator(8, 0, 0, weekdays), new RestrictedDailyIterator(9, 0, 0, weekend) } );
RestrictedDailyIterator 類很像 DailyIterator,只不過它限制為只在一周的特定日子里運行,而一個 CompositeIterator 類取得一組 ScheduleIterators,并將日期正確排列到單個計劃中。
有許多計劃是 cron 無法生成的,但是 ScheduleIterator 實現卻可以。例如,“每個月的最后一天”描述的計劃可以用標準 Java 日歷算法來實現(用 Calendar 類),而用 cron 則無法表達它。應用程序甚至無需使用 Calendar 類。在本文的源代碼(請參閱 參考資料)中,我加入了一個安全燈控制器的例子,它按“在日落之前 15 分鐘開燈”這一計劃運行。這個實現使用了 Calendrical Calculations Software Package,用于計算當地(給定經度和緯度)的日落時間。
實時保證
在編寫使用計劃的應用程序時,一定要了解框架在時間方面有什么保證。我的任務是提前還是延遲執行?如果有提前或者延遲,偏差最大值是多少?不幸的是,對這些問題沒有簡單的答案。不過在實際中,它的行為對于很多應用程序已經足夠了。下面的討論假設系統時鐘是正確的。
因為 Scheduler 將計劃委托給 Timer 類,Scheduler 可以做出的實時保證與 Timer 的一樣。Timer 用 Object.wait(long) 方法計劃任務。當前線程要等待直到喚醒它,喚醒可能出于以下原因之一:
1.另一個線程調用對象的 notify() 或者 notifyAll() 方法。
2.線程被另一個線程中斷。
3.在沒有通知的情況下,線程被喚醒(稱為 spurious wakeup,Joshua Bloch 的 Effective Java Programming Language Guide 一書中 Item 50 對其進行了描述 。
4.規定的時間已到。
對于 Timer 類來說,第一種可能性是不會發生的,因為對其調用 wait() 的對象是私有的。即便如此,Timer 實現仍然針對前三種提前喚醒的原因進行了保護,這樣保證了線程在規定時間后才喚醒。目前,Object.wait(long) 的文檔注釋聲明,它會在規定的時間“前后”蘇醒,所以線程有可能提前喚醒。在本例中,Timer 會讓另一個 wait() 執行(scheduledExecutionTime - System.currentTimeMillis())毫秒,從而保證任務永遠不會提前執行。任務是否會延遲執行呢?會的。延遲執行有兩個主要原因:線 程計劃和垃圾收集。
Java 語言規范故意沒有對線程計劃做嚴格的規定。這是因為 Java 平臺是通用的,并針對于大范圍的硬件及其相關的操作系統。雖然大多數 JVM 實現都有公平的線程調度程序,但是這一點沒有任何保證 —— 當然,各個實現都有不同的為線程分配處理器時間的策略。因此,當 Timer 線程在分配的時間后喚醒時,它實際執行其任務的時間取決于 JVM 的線程計劃策略,以及有多少其他線程競爭處理器時間。因此,要減緩任務的延遲執行,應該將應用程序中可運行的線程數降至最少。為了做到這一點,可以考慮在 一個單獨的 JVM 中運行調度程序。
對于創建大量對象的大型應用程序,JVM 花在垃圾收集(GC)上的時間會非常多。默認情況下,進行 GC 時,整個應用程序都必須等待它完成,這可能要有幾秒鐘甚至更長的時間(Java 應用程序啟動器的命令行選項 -verbose:gc 將導致向控制臺報告每一次 GC 事件)。要將這些由 GC 引起的暫停(這可能會影響快速任務的執行)降至最少,應該將應用程序創建的對象的數目降至最低。同樣,在單獨的 JVM 中運行計劃代碼是有幫助的。同時,可以試用幾個微調選項以盡可能地減少 GC 暫停。例如,增量 GC 會盡量將主收集的代價分散到幾個小的收集上。當然這會降低 GC 的效率,但是這可能是時間計劃的一個可接受的代價。
被計劃到什么時候?
如果任務本身能監視并記錄所有延遲執行的實例,那么對于確定任務是否能按時運行會很有幫助。SchedulerTask 類似于 TimerTask,有一個 scheduledExecutionTime() 方法,它返回計劃任務最近一次執行的時間。在任務的 run() 方法開始時,對表達式 System.currentTimeMillis() - scheduledExecutionTime() 進行判斷,可以讓您確定任務延遲了多久執行(以毫秒為單位)。可以記錄這個值,以便生成一個關于延遲執行的分布統計。可以用這個值決定任務應當采取什么動 作 —— 例如,如果任務太遲了,那么它可能什么也不做。在遵循上述原則的情況下,如果應用程序需要更嚴格的時間保證,可參考 Java 的實時規范。
結束語
在本文中,我介紹了 Java 定時器框架的一個簡單增強,它使得靈活的計劃策略成為可能。新的框架實質上是更通用的 cron —— 事實上,將 cron 實現為一個 ScheduleIterator 接口,用以替換單純的 Java cron,這是非常有用的。雖然沒有提供嚴格的實時保證,但是許多需要計劃定期任務的通用 Java 應用程序都可以使用這一框架。
參考資料
·下載本文中使用的 源代碼。
·“Tuning Garbage Collection with the 1.3.1 Java Virtual Machine”是 Sun 的一篇非常有用的文章,它給出了關于如何最小化 GC 暫停時間的提示。
·要獲得 developerWorks 中有關 GC 的更多信息,請參閱以下文章:
“Java 理論與實踐:垃圾收集簡史” (2003 年 10 月)。
“Mash that trash”(2003 年 7 月)。
“Fine-tuning Java garbage collection performance”(2003 年 1 月)。
“Sensible sanitation, Part 1”(2002 年 8 月)。
“Sensible sanitation, Part 2”(2002 年 8 月)。
“Sensible sanitation, Part 3”(2002 年 9 月)。
·在“Java 理論與實踐:并發在一定程度上使一切變得簡單”(developerWorks, 2002 年 11 月)中,Brian Goetz 討論了 Doug Lea 的 util.concurrent 庫,這是一個并發實用工具類的寶庫。
·Brian Goetz 的另一篇文章“Threading lightly, Part 2: Reducing contention”(developerWorks,2001 年 9 月)分析了線程競用以及如何減少它。
本文轉自:http://blog.csdn.net/zwhfyy/article/details/1620840