1. ตัวจับเวลา - พื้นฐาน
TimerและTimerTaskเป็นคลาส java util ที่ใช้เพื่อจัดกำหนดการงานในเธรดพื้นหลัง คำไม่กี่คำ - TimerTaskเป็นงานที่จะดำเนินการและจับเวลาเป็นกำหนดการ
2. กำหนดเวลางานหนึ่งครั้ง
2.1. หลังจากได้รับความล่าช้า
เริ่มต้นด้วยการรันงานเดียวด้วยตัวจับเวลา :
@Test public void givenUsingTimer_whenSchedulingTaskOnce_thenCorrect() { TimerTask task = new TimerTask() { public void run() { System.out.println("Task performed on: " + new Date() + "n" + "Thread's name: " + Thread.currentThread().getName()); } }; Timer timer = new Timer("Timer"); long delay = 1000L; timer.schedule(task, delay); }
ตอนนี้นี้ดำเนินงานหลังจากที่ล่าช้าบางอย่างให้เป็นพารามิเตอร์ที่สองของตารางเวลา ()วิธีการ เราจะดูในหัวข้อถัดไปว่าจะกำหนดเวลางานตามวันและเวลาที่กำหนดได้อย่างไร
โปรดทราบว่าหากเรากำลังเรียกใช้นี่เป็นการทดสอบ JUnit เราควรเพิ่มการเรียกThread.sleep (delay * 2)เพื่อให้เธรดของ Timer ทำงานก่อนที่การทดสอบ Junit จะหยุดดำเนินการ
2.2. ณ วันที่และเวลาที่กำหนด
ตอนนี้เรามาดูตัวจับเวลา # กำหนดการ (TimerTask วัน)วิธีการที่จะใช้เวลาวันแทนที่จะเป็นนานเป็นพารามิเตอร์ที่สองทำให้เราสามารถกำหนดเวลาการงานในทันทีบางอย่างมากกว่าหลังจากที่ล่าช้า
คราวนี้ลองจินตนาการว่าเรามีฐานข้อมูลเก่าและเราต้องการย้ายข้อมูลไปยังฐานข้อมูลใหม่ด้วยสคีมาที่ดีกว่า
เราสามารถสร้างคลาสDatabaseMigrationTaskที่จะจัดการกับการย้ายข้อมูลนั้น:
public class DatabaseMigrationTask extends TimerTask { private List oldDatabase; private List newDatabase; public DatabaseMigrationTask(List oldDatabase, List newDatabase) { this.oldDatabase = oldDatabase; this.newDatabase = newDatabase; } @Override public void run() { newDatabase.addAll(oldDatabase); } }
สำหรับความเรียบง่ายที่เรากำลังเป็นตัวแทนของทั้งสองฐานข้อมูลโดยรายการของสตริง พูดง่ายๆคือการย้ายข้อมูลของเราประกอบด้วยการใส่ข้อมูลจากรายการแรกลงในรายการที่สอง
ในการย้ายข้อมูลนี้ในทันทีที่ต้องการเราจะต้องใช้วิธีกำหนดการ()เวอร์ชันที่โอเวอร์โหลด :
List oldDatabase = Arrays.asList("Harrison Ford", "Carrie Fisher", "Mark Hamill"); List newDatabase = new ArrayList(); LocalDateTime twoSecondsLater = LocalDateTime.now().plusSeconds(2); Date twoSecondsLaterAsDate = Date.from(twoSecondsLater.atZone(ZoneId.systemDefault()).toInstant()); new Timer().schedule(new DatabaseMigrationTask(oldDatabase, newDatabase), twoSecondsLaterAsDate);
อย่างที่เราเห็นเราให้งานการโอนย้ายและวันที่ดำเนินการกับเมธอดSchedule ()
จากนั้นการโอนย้ายจะดำเนินการตามเวลาที่ระบุโดยtwoSecondsLater :
while (LocalDateTime.now().isBefore(twoSecondsLater)) { assertThat(newDatabase).isEmpty(); Thread.sleep(500); } assertThat(newDatabase).containsExactlyElementsOf(oldDatabase);
ในขณะที่เราอยู่ก่อนหน้านี้การโยกย้ายจะไม่เกิดขึ้น
3. กำหนดเวลางานที่ทำซ้ำได้
ตอนนี้เราได้พูดถึงวิธีกำหนดเวลาการดำเนินการงานเดียวแล้วมาดูวิธีจัดการกับงานที่ทำซ้ำได้
อีกครั้งมีความเป็นไปได้หลายอย่างที่นำเสนอโดยคลาสTimer : เราสามารถตั้งค่าการทำซ้ำเพื่อสังเกตความล่าช้าคงที่หรืออัตราคงที่
คงที่ล่าช้าหมายความว่าการดำเนินการจะเริ่มต้นในช่วงเวลาหลังจากที่ขณะนี้การดำเนินการที่ผ่านมาเริ่มต้นถึงแม้ว่ามันจะถูกเลื่อนออกไป (จึงเป็นตัวเองล่าช้า)
สมมติว่าเราต้องการกำหนดเวลางานบางอย่างทุกๆสองวินาทีและการดำเนินการครั้งแรกใช้เวลาหนึ่งวินาทีและงานที่สองใช้เวลาสองครั้ง แต่ล่าช้าไปหนึ่งวินาที จากนั้นการดำเนินการที่สามจะเริ่มในวินาทีที่ห้า:
0s 1s 2s 3s 5s |--T1--| |-----2s-----|--1s--|-----T2-----| |-----2s-----|--1s--|-----2s-----|--T3--|
บนมืออื่น ๆ , อัตราดอกเบี้ยคงที่หมายความว่าแต่ละการดำเนินการจะเคารพกำหนดการเริ่มต้นไม่ว่าหากการดำเนินการก่อนหน้านี้ได้ถูกเลื่อนออกไป
ลองใช้ตัวอย่างก่อนหน้านี้ซ้ำด้วยอัตราคงที่งานที่สองจะเริ่มหลังจากสามวินาที (เนื่องจากความล่าช้า) แต่อันที่สามหลังจากสี่วินาที (ตามกำหนดการเริ่มต้นของการดำเนินการหนึ่งครั้งทุกๆสองวินาที):
0s 1s 2s 3s 4s |--T1--| |-----2s-----|--1s--|-----T2-----| |-----2s-----|-----2s-----|--T3--|
หลักการทั้งสองนี้ได้รับการกล่าวถึงเรามาดูวิธีการใช้งานกัน
ในการใช้การตั้งเวลาการหน่วงเวลาคงที่มีการโอเวอร์โหลดอีกสองวิธีของกำหนดการ ()โดยแต่ละวิธีจะใช้พารามิเตอร์พิเศษที่ระบุระยะเวลาเป็นมิลลิวินาที
ทำไมเกินสอง? เนื่องจากยังมีความเป็นไปได้ที่จะเริ่มงานในช่วงเวลาหนึ่งหรือหลังจากความล่าช้าบางอย่าง
สำหรับการจัดกำหนดการอัตราคงที่เรามีสองวิธีscheduleAtFixedRate () ซึ่งใช้เวลาเป็นมิลลิวินาที อีกครั้งเรามีวิธีการหนึ่งในการเริ่มงานในวันที่และเวลาที่กำหนดและอีกวิธีหนึ่งเพื่อเริ่มงานหลังจากความล่าช้าที่กำหนด
นอกจากนี้ยังควรค่าแก่การกล่าวถึงด้วยว่าหากงานใช้เวลามากกว่าระยะเวลาในการดำเนินการงานจะทำให้การดำเนินการทั้งหมดล่าช้าไม่ว่าเราจะใช้ความล่าช้าคงที่หรืออัตราคงที่
3.1. ด้วยความล่าช้าคงที่
ตอนนี้สมมติว่าเราต้องการใช้ระบบจดหมายข่าวโดยส่งอีเมลไปยังผู้ติดตามของเราทุกสัปดาห์ ในกรณีนี้งานซ้ำ ๆ ดูเหมือนจะเหมาะ
ดังนั้นเรามากำหนดเวลารับจดหมายข่าวทุก ๆ วินาทีซึ่งโดยพื้นฐานแล้วเป็นการส่งสแปม แต่เนื่องจากการส่งนั้นเป็นของปลอมเราจึงพร้อมที่จะไป
ก่อนอื่นมาออกแบบNewsletterTask :
public class NewsletterTask extends TimerTask { @Override public void run() { System.out.println("Email sent at: " + LocalDateTime.ofInstant(Instant.ofEpochMilli(scheduledExecutionTime()), ZoneId.systemDefault())); } }
ทุกครั้งที่ดำเนินการงานจะพิมพ์เวลาที่กำหนดไว้ซึ่งเรารวบรวมโดยใช้เมธอดTimerTask # ScheduledExecutionTime ()
ถ้าเราต้องการกำหนดเวลางานนี้ทุก ๆ วินาทีในโหมดหน่วงเวลาคงที่? เราจะต้องใช้กำหนดการที่มากเกินไป() ที่เราพูดถึงก่อนหน้านี้:
new Timer().schedule(new NewsletterTask(), 0, 1000); for (int i = 0; i < 3; i++) { Thread.sleep(1000); }
แน่นอนเราทำการทดสอบเพียงไม่กี่ครั้งเท่านั้น:
Email sent at: 2020-01-01T10:50:30.860 Email sent at: 2020-01-01T10:50:31.860 Email sent at: 2020-01-01T10:50:32.861 Email sent at: 2020-01-01T10:50:33.861
อย่างที่เราเห็นมีเวลาอย่างน้อยหนึ่งวินาทีระหว่างการดำเนินการแต่ละครั้ง แต่บางครั้งการดำเนินการจะล่าช้าเป็นมิลลิวินาที ปรากฏการณ์นั้นเกิดจากการตัดสินใจของเราที่จะใช้การทำซ้ำแบบหน่วงเวลาคงที่
3.2. ด้วยอัตราคงที่
ทีนี้จะเกิดอะไรขึ้นถ้าเราใช้การทำซ้ำอัตราคงที่? จากนั้นเราจะต้องใช้เมธอดScheduAtFixedRate () :
new Timer().scheduleAtFixedRate(new NewsletterTask(), 0, 1000); for (int i = 0; i < 3; i++) { Thread.sleep(1000); }
This time, executions are not delayed by the previous ones:
Email sent at: 2020-01-01T10:55:03.805 Email sent at: 2020-01-01T10:55:04.805 Email sent at: 2020-01-01T10:55:05.805 Email sent at: 2020-01-01T10:55:06.805
3.3. Schedule a Daily Task
Next, let's run a task once a day:
@Test public void givenUsingTimer_whenSchedulingDailyTask_thenCorrect() { TimerTask repeatedTask = new TimerTask() { public void run() { System.out.println("Task performed on " + new Date()); } }; Timer timer = new Timer("Timer"); long delay = 1000L; long period = 1000L * 60L * 60L * 24L; timer.scheduleAtFixedRate(repeatedTask, delay, period); }
4. Cancel Timer and TimerTask
An execution of a task can be canceled in a few ways:
4.1. Cancel the TimerTask Inside Run
By calling the TimerTask.cancel() method inside the run() method's implementation of the TimerTask itself:
@Test public void givenUsingTimer_whenCancelingTimerTask_thenCorrect() throws InterruptedException { TimerTask task = new TimerTask() { public void run() { System.out.println("Task performed on " + new Date()); cancel(); } }; Timer timer = new Timer("Timer"); timer.scheduleAtFixedRate(task, 1000L, 1000L); Thread.sleep(1000L * 2); }
4.2. Cancel the Timer
By calling the Timer.cancel() method on a Timer object:
@Test public void givenUsingTimer_whenCancelingTimer_thenCorrect() throws InterruptedException { TimerTask task = new TimerTask() { public void run() { System.out.println("Task performed on " + new Date()); } }; Timer timer = new Timer("Timer"); timer.scheduleAtFixedRate(task, 1000L, 1000L); Thread.sleep(1000L * 2); timer.cancel(); }
4.3. Stop the Thread of the TimerTask Inside Run
You can also stop the thread inside the run method of the task, thus canceling the entire task:
@Test public void givenUsingTimer_whenStoppingThread_thenTimerTaskIsCancelled() throws InterruptedException { TimerTask task = new TimerTask() { public void run() { System.out.println("Task performed on " + new Date()); // TODO: stop the thread here } }; Timer timer = new Timer("Timer"); timer.scheduleAtFixedRate(task, 1000L, 1000L); Thread.sleep(1000L * 2); }
Notice the TODO instruction in the run implementation – in order to run this simple example, we'll need to actually stop the thread.
In a real-world custom thread implementation, stopping the thread should be supported, but in this case we can ignore the deprecation and use the simple stop API on the Thread class itself.
5. Timer vs ExecutorService
You can also make good use of an ExecutorService to schedule timer tasks, instead of using the timer.
Here's a quick example of how to run a repeated task at a specified interval:
@Test public void givenUsingExecutorService_whenSchedulingRepeatedTask_thenCorrect() throws InterruptedException { TimerTask repeatedTask = new TimerTask() { public void run() { System.out.println("Task performed on " + new Date()); } }; ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); long delay = 1000L; long period = 1000L; executor.scheduleAtFixedRate(repeatedTask, delay, period, TimeUnit.MILLISECONDS); Thread.sleep(delay + period * 3); executor.shutdown(); }
So what are the main differences between the Timer and the ExecutorService solution:
- Timer can be sensitive to changes in the system clock; ScheduledThreadPoolExecutor is not
- Timer has only one execution thread; ScheduledThreadPoolExecutor can be configured with any number of threads
- Runtime Exceptions thrown inside the TimerTask kill the thread, so following scheduled tasks won't run further; with ScheduledThreadExecutor – the current task will be canceled, but the rest will continue to run
6. Conclusion
บทช่วยสอนนี้แสดงให้เห็นถึงหลายวิธีที่คุณสามารถใช้ประโยชน์จากโครงสร้างพื้นฐานของTimerและTimerTask ที่เรียบง่าย แต่ยืดหยุ่นใน Java เพื่อการจัดกำหนดการงาน แน่นอนว่ามีโซลูชันที่ซับซ้อนและสมบูรณ์กว่านี้ในโลก Java หากคุณต้องการเช่นไลบรารี Quartz แต่นี่เป็นจุดเริ่มต้นที่ดีมาก
การใช้งานตัวอย่างเหล่านี้สามารถพบได้ในโปรเจ็กต์ GitHub ซึ่งเป็นโปรเจ็กต์ที่ใช้ Eclipse ดังนั้นจึงควรอิมพอร์ตและรันได้ง่ายเหมือนเดิม