- 浏览: 455596 次
- 性别:
- 来自: 江门
-
文章分类
最新评论
-
ctz_131415:
谁能告诉我这个分页控件如何添加到窗口上?谢谢了
C# Winform DataGridView 公共分页实现 -
omymy:
FrmPage_Shown是放在页面加载时的load事件里的, ...
C# Winform DataGridView 公共分页实现 -
qie:
如何使用该分页控件?
C# Winform DataGridView 公共分页实现 -
anyanlonga:
看地图干什么用啊,如果查公交的话,自认为还是白度好
Android 上创建和地图相关的应用,用 Google 地图 API 好,还是百度等第三方地图 API 好 -
dedepro:
果然很强。
CSS层 固定置顶部兼容IE6等以上浏览器
所有类型的 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; 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.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; 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中HashMap的用法
2012-04-10 11:31 1108import java.util.Map; import ... -
Eclipse-jee-galileo的简单应用配置(包含Tomcat)
2012-03-23 17:43 1首先:下载 JDK 6,Eclip ... -
JDK官方下载
2011-11-27 22:54 872JDK1.6官方下载_JDK6官方下载地址:http:// ... -
使用Servlet Filter来防止SQL注入的方法
2010-08-28 14:29 1839SQL注入是比较常见的网络攻击方式之一,它不是利用操作系统的B ... -
避免XPath 注入的危险
2010-08-28 14:27 1489随着简单 XML API、Web 服 ... -
Xpath注入攻击及其防御技术研究
2010-08-28 14:25 13961 Xpath注入攻击概述 1.1 Xpath定义 近 ... -
突破SQL注入限制的一点想法
2010-08-28 14:19 1062突然想我们是否可以用什么方法绕过SQL注入的限制呢?到网上考察 ... -
JPanel上设置背景图片
2010-08-28 14:12 1507import java.awt.*; import javax ... -
通过Java Swing看透MVC设计模式
2010-08-28 14:11 977一个好的用户界面(GUI) ... -
JTabbedPanel的美化标签
2010-08-28 14:09 4568么现在来说下用法吧,首先附上网上找来的一段代码(其实在csdn ... -
jtree 默认展开树的全部节点
2010-08-28 14:06 5708public static void expandTr ... -
jTree 背景图片.节点颜色
2010-08-28 14:05 1631import java.awt.*; import java. ... -
JTree用法
2010-08-28 14:04 1325一、JTree用法及JTree使用经验总结 import ja ... -
Swing-JTree树模型的操作
2010-08-28 14:03 2419本文介绍Java Swing中的JTree模型,介绍如何创建、 ... -
常用的数据库连接写法和下载地址
2010-08-28 14:01 8981. MySQL(http://www.mysql.com)m ... -
java中常见的日期时间类
2010-08-28 13:20 830java中常见的日期时间类 以及时间转换 java中常见的 ... -
计算任意2个日期内的工作日(没有考虑到国定假日
2010-08-28 13:08 1125主要思路: 对于任意2个日期比如:date_start=20 ... -
Java获取各种常用时间方法
2010-08-28 13:07 760http://gigixin.iteye.com/blog/4 ... -
计算从beginDate到endDate的工作日
2010-08-28 13:06 1500public static int[] work_rest_a ... -
Java时间格式转换大全
2010-08-28 13:05 559import java.text.*; import java ...
相关推荐
内容概要:Elasticsearch是一款强大且灵活的搜索和数据分析工具。文中介绍了其核心技术如分布式存储、实时搜索、全文检索、数据分析等。通过对基础概念的学习,如索引、文档、类型、映射的理解,结合实战案例解析,重点展示了Elasticsearch在电商业务商品搜索引擎构建以及高效日志管理系统部署方面的实际运用方法和技术细节。此外,围绕性能优化展开了讨论,强调了诸如合理的分片和副本配置、有效运用内部缓存机制和精心规划集群资源配置等一系列措施的重要性。 适合人群:从事IT行业的中级及以上技术水平从业者,尤其是那些负责大数据处理、分布式系统的架构师及工程师。 使用场景及目标:①希望掌握利用Elasticsearch快速实现高效的搜索与分析应用的方法论和技术路径;②旨在通过实例学习到针对不同应用场景(如电商网站、日志分析)如何正确配置系统参数、优化集群表现,进而达成更好的用户体验或运营效率;③寻求提升系统稳定性、可靠性并解决可能出现的问题。 其他说明:本文不仅仅讲述了理论知识,还有详实的具体操作指南,帮助读者在实践中深入理解Elasticsearch的能力,并鼓励他们在自己的项目中积极探索更
基于Matlab的双三方演化博弈与Lotka-Volterra模型稳定点分析、相位图绘制与仿真代码实现,基于Matlab的双三方演化博弈与Lotka-Volterra模型:稳定点分析、相位图绘制与仿真代码实践,matlab:双或三方演化博弈,lotka-Volterra 1.双方演化博弈:代分析稳定点分析,代绘制相位图,matlab仿真图代码 2.三方演化博弈:代分析稳定点分析,代绘制相位图,matlab仿真图代码3.lotka-Volterra模型 ,核心关键词:Matlab; 双或三方演化博弈; 稳定点分析; 相位图; 仿真图代码; Lotka-Volterra模型,MATLAB仿真:双三方演化博弈与Lotka-Volterra模型的稳定点分析与相位图绘制
【资源介绍】 1、该资源包括项目的全部源码,下载可以直接使用! 2、本项目适合作为计算机、数学、电子信息等专业的课程设计、期末大作业和毕设项目,也可以作为小白实战演练和初期项目立项演示的重要参考借鉴资料。 3、本资源作为“学习资料”如果需要实现其他功能,需要能看懂代码,并且热爱钻研和多多调试实践。 基于词袋模型及神经网络的文本分类算法新版源码+说明+数据.zip 基于词袋模型及神经网络的文本分类算法新版源码+说明+数据.zip 基于词袋模型及神经网络的文本分类算法新版源码+说明+数据.zip 基于词袋模型及神经网络的文本分类算法新版源码+说明+数据.zip 基于词袋模型及神经网络的文本分类算法新版源码+说明+数据.zip 基于词袋模型及神经网络的文本分类算法新版源码+说明+数据.zip 基于词袋模型及神经网络的文本分类算法新版源码+说明+数据.zip 基于词袋模型及神经网络的文本分类算法新版源码+说明+数据.zip 基于词袋模型及神经网络的文本分类算法新版源码+说明+数据.zip 基于词袋模型及神经网络的文本分类算法新版源码+说明+数据.zip 基于词袋模型及神经网络的文本分类算法新版源码+说明+数据.zip 基于词袋模型及神经网络的文本分类算法新版源码+说明+数据.zip
海神之光上传的视频是由对应的完整代码运行得来的,完整代码皆可运行,亲测可用,适合小白; 1、从视频里可见完整代码的内容 主函数:main.m; 调用函数:其他m文件;无需运行 运行结果效果图; 2、代码运行版本 Matlab 2019b;若运行有误,根据提示修改;若不会,私信博主; 3、运行操作步骤 步骤一:将所有文件放到Matlab的当前文件夹中; 步骤二:双击打开main.m文件; 步骤三:点击运行,等程序运行完得到结果; 4、仿真咨询 如需其他服务,可私信博主; 4.1 博客或资源的完整代码提供 4.2 期刊或参考文献复现 4.3 Matlab程序定制 4.4 科研合作
海神之光上传的视频是由对应的完整代码运行得来的,完整代码皆可运行,亲测可用,适合小白; 1、从视频里可见完整代码的内容 主函数:main.m; 调用函数:其他m文件;无需运行 运行结果效果图; 2、代码运行版本 Matlab 2019b;若运行有误,根据提示修改;若不会,私信博主; 3、运行操作步骤 步骤一:将所有文件放到Matlab的当前文件夹中; 步骤二:双击打开main.m文件; 步骤三:点击运行,等程序运行完得到结果; 4、仿真咨询 如需其他服务,可私信博主; 4.1 博客或资源的完整代码提供 4.2 期刊或参考文献复现 4.3 Matlab程序定制 4.4 科研合作
COMSOL模拟放电电极击穿空气过程:电场分布与击穿间隙电压计算分析,COMSOL模拟放电电极击穿空气过程:电场分布与击穿间隙电压计算分析,comsol放电电极击穿空气模拟,计算击穿间隙的电压,周围附近的电场 ,关键词:COMSOL放电电极;击穿空气模拟;计算;击穿间隙电压;周围附近电场;电场分布。,COMSOL模拟放电电极击穿空气过程,计算电压与电场分布分析
高压柔性输电系统:6脉冲与12脉冲晶闸管控制的HVDC仿真模型详细说明文档,高压柔性输电系统:6脉冲与12脉冲晶闸管控制的HVDC仿真模型详解说明文档,高压柔性输电系统6脉冲,12脉冲晶闸管控制HVDC的仿真模型,说明文档 ,高压柔性输电系统; 6脉冲HVDC; 12脉冲晶闸管控制; 仿真模型; 说明文档,高压柔性输电系统仿真模型:6/12脉冲晶闸管控制HVDC说明文档
海神之光上传的视频是由对应的完整代码运行得来的,完整代码皆可运行,亲测可用,适合小白; 1、从视频里可见完整代码的内容 主函数:main.m; 调用函数:其他m文件;无需运行 运行结果效果图; 2、代码运行版本 Matlab 2019b;若运行有误,根据提示修改;若不会,私信博主; 3、运行操作步骤 步骤一:将所有文件放到Matlab的当前文件夹中; 步骤二:双击打开main.m文件; 步骤三:点击运行,等程序运行完得到结果; 4、仿真咨询 如需其他服务,可私信博主; 4.1 博客或资源的完整代码提供 4.2 期刊或参考文献复现 4.3 Matlab程序定制 4.4 科研合作
近年来,文本驱动的视频生成 (Brooks 等人 2024;Hunyuan 2024) 取得了显著增长。然而,评估这些文本驱动的AI生成视频带来了独特且日益严峻的挑战。这些挑战主要源于两个关键问题:(1)需要精确的视频与文本对齐,特别是在处理复杂和长文本提示时;(2)出现了一些在自然生成视频中不常见的独特失真现象,例如不规则运动模式和物体。 随着新一代视频模型的发展,这些挑战变得更加突出。这些新一代模型以 Sora (Brooks 等人 2024) 的出现为标志,在生成质量上相比以往模型有了显著提升,其特点在于丰富的细节和内容,如 Kling (快手 2024) 、Gen-3-alpha (Runway 2024) 、Vidu (圣书 2024) 等。与之前的 AIGC 视频相比,这些模型支持 更长且更复杂的文本提示(通常超过200个字符),以及更复杂的运动模式和更长的持续时间(通常超过5秒,帧率为24帧每秒) 。如图 [fig:1] 所示,这些丰富的内容对评估者的理解视频动态及其与复杂文本语义关系的能力提出了更高的要求。 为了应对这一问题,我们引入了 Conten
在B站看黑马程序员,自学python,整理的个人笔记
传统永磁同步电机FOC离散化Simulink模型实践指南:高效性能与传递函数离散化推导文档附赠,传统永磁同步电机FOC离散化Simulink模型实战解析及传递函数离散化推导入门指南,传统永磁同步电机的FOC离散化simulink模型,效果较好。 附赠传递函数离散化推导的文档,初学者可以入手。 ,传统永磁同步电机; FOC离散化; Simulink模型; 传递函数离散化; 推导文档。,FOC离散化Simulink模型:永磁同步电机高效控制与传递函数离散化解析
内容概要:本文由360集团创始人周鸿祎撰写,深入探讨了DeepSeek这一前沿AI技术及其对各行各业所带来的巨大机遇。文中详细阐述了人工智能的发展历程,特别是大模型的演进,并指出了DeepSeek如何在技术和用户体验方面取得重大突破,引领新的工业革命,以及中国在该领域的创新和发展前景。同时介绍了如何借助DeepSeek实现具体的企业应用,涵盖知识库建设、智能体开发等多个方面的实践经验。 适用人群:针对政府机构、企业和创新创业者的高级管理层和技术领导者,旨在提供对当前AI前沿技术和未来发展策略的理解。 使用场景及目标:适用于希望通过先进技术提升竞争力的单位或个人;目的在于引导读者建立正确的AI意识,了解最新的技术动向和实施路径,为未来的战略规划打下坚实的基础。 其他说明:文档还强调了在全球范围内争夺大模型主导地位的竞争环境下,中国企业应该如何抓住机遇实现快速发展,以及如何克服现有挑战,确保安全可靠的应用。
APP测试基础流程
建设工程管理数字孪生平台解决方案.docx
海神之光上传的视频是由对应的完整代码运行得来的,完整代码皆可运行,亲测可用,适合小白; 1、从视频里可见完整代码的内容 主函数:main.m; 调用函数:其他m文件;无需运行 运行结果效果图; 2、代码运行版本 Matlab 2019b;若运行有误,根据提示修改;若不会,私信博主; 3、运行操作步骤 步骤一:将所有文件放到Matlab的当前文件夹中; 步骤二:双击打开main.m文件; 步骤三:点击运行,等程序运行完得到结果; 4、仿真咨询 如需其他服务,可私信博主; 4.1 博客或资源的完整代码提供 4.2 期刊或参考文献复现 4.3 Matlab程序定制 4.4 科研合作
串口助手
深度学习-卷积神经网络的猫狗数据集
基于Python的Django-vue社会主义核心价值观视角下电商平台型社会责任评价研究源码-演示视频 项目关键技术 开发工具:Pycharm 编程语言: python 数据库: MySQL5.7+ 后端技术:Django 前端技术:HTML 关键技术:HTML、MYSQL、Python 数据库工具:Navicat、SQLyog
1、以上文章可用于参考,请勿直接抄袭,学习、当作参考文献可以,主张借鉴学习 2、资源本身不含 对应项目代码,如需完整项目源码,请私信博主获取
scala-intellij-bin-2024.1.1.zip