טיימר ג'אווה

1. טיימר - היסודות

שָׁעוֹן עֶצֶר ו TimerTask הם שיעורי שימוש בג'אווה המשמשים לתזמון משימות בשרשור הרקע. בכמה מילים - TimerTask זו המשימה לבצע ו שָׁעוֹן עֶצֶר הוא המתזמן.

2. קבעו משימה פעם אחת

2.1. לאחר עיכוב נתון

נתחיל בפשטות מריצה משימה אחת בעזרת א שָׁעוֹן עֶצֶר:

@Test public void givenUsingTimer_whenSchedulingTaskOnce_thenCorrect () {TimerTask task = new TimerTask () {public void run () {System.out.println ("המשימה בוצעה בתאריך:" + תאריך חדש () + "n" + "שם החוט:" + Thread.currentThread (). GetName ()); }}; טיימר טיימר = טיימר חדש ("טיימר"); עיכוב ארוך = 1000L; timer.schedule (משימה, עיכוב); }

עַכשָׁיו, זה מבצע את המשימה לאחר עיכוב מסוים, נתון כפרמטר השני של ה- לוח זמנים() שיטה. נראה בסעיף הבא כיצד לתזמן משימה בתאריך ובשעה מסוימים.

שים לב שאם אנו מריצים זו בדיקת JUnit, עלינו להוסיף a Thread.sleep (עיכוב * 2) התקשר לאפשר לשרשור הטיימר להפעיל את המשימה לפני שמבחן ג'וניט מפסיק לבצע.

2.2. בתאריך ושעה נתון

עכשיו, בואו נראה את לוח זמנים מספר טיימר (TimerTask, תאריך) שיטה, שלוקחת א תַאֲרִיך במקום ארוך כפרמטר השני שלו, המאפשר לנו לתזמן את המשימה ברגע מסוים, ולא לאחר עיכוב.

הפעם, בואו נדמיין שיש לנו בסיס נתונים ישן מדור קודם ואנחנו רוצים להעביר את הנתונים שלו למסד נתונים חדש עם סכמה טובה יותר.

נוכל ליצור DatabaseMigrationTask כיתה שתטפל בהעברה זו:

class class DatabaseMigrationTask מרחיב את TimerTask {רשימה פרטית oldDatabase; רשימה פרטית newDatabase; Public DatabaseMigrationTask (רשימת oldDatabase, List newDatabase) {this.oldDatabase = oldDatabase; this.newDatabase = newDatabase; } @ הריצה בטוחה ציבורית () {newDatabase.addAll (oldDatabase); }}

לשם הפשטות, אנו מייצגים את שני מאגרי המידע על ידי a רשימה שֶׁל חוּט. במילים פשוטות, ההגירה שלנו מורכבת מהכנסת הנתונים מהרשימה הראשונה לשנייה.

כדי לבצע העברה זו ברגע הרצוי, נצטרך להשתמש בגרסה העמוסה של ה- לוח זמנים() שיטה:

רשום oldDatabase = Arrays.asList ("הריסון פורד", "קארי פישר", "מארק המיל"); רשימת newDatabase = ArrayList חדש (); LocalDateTime twoSecondsLater = LocalDateTime.now (). PlusSeconds (2); תאריך twoSecondsLaterAsDate = Date.from (twoSecondsLater.atZone (ZoneId.systemDefault ()). ToInstant ()); טיימר חדש (). לוח זמנים (חדש DatabaseMigrationTask (oldDatabase, newDatabase), twoSecondsLaterAsDate);

כפי שאנו רואים, אנו נותנים את משימת ההעברה וכן את תאריך הביצוע ל- לוח זמנים() שיטה.

לאחר מכן, ההעברה מתבצעת בזמן שצוין על ידי שני שניות מאוחר יותר:

בעוד (LocalDateTime.now (). isBefore (twoSecondsLater)) {assertThat (newDatabase) .isEmpty (); Thread.sleep (500); } assertThat (newDatabase) .containsExactlyElementsOf (oldDatabase);

בזמן שאנחנו לפני רגע זה, ההגירה אינה מתרחשת.

3. קבע משימה חוזרת

עכשיו כשסקרנו כיצד לתזמן ביצוע יחיד של משימה, בואו נראה כיצד להתמודד עם משימות הניתנות לחזרה.

שוב, יש מספר אפשרויות המוצעות על ידי שָׁעוֹן עֶצֶר בכיתה: אנו יכולים להגדיר את החזרה כך שתצפה בעיכוב קבוע או בקצב קבוע.

עיכוב קבוע פירושו שהביצוע יתחיל פרק זמן לאחר הרגע בו הושמעה הביצוע האחרון, גם אם הוא עוכב (ולכן הוא עצמו מתעכב).

נניח שאנחנו רוצים לתזמן משימה כל שתי שניות, ושהביצוע הראשון לוקח שנייה אחת והשנייה אורכת שתיים אבל מתעכבת בשנייה אחת. ואז, ההוצאה להורג השלישית תתחיל בשנייה החמישית:

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:

מחלקה ציבורית NewsletterTask מרחיבה את TimerTask {@Override public void run () {System.out.println ("דוא"ל נשלח בכתובת:" + LocalDateTime.ofInstant (Instant.ofEpochMilli (planningExecutionTime ()), ZoneId.systemDefault ())); }}

בכל פעם שהיא מבצעת, המשימה תדפיס את השעה המתוזמנת שלה, אותה אנו אוספים באמצעות ה- TimerTask # מתוכנןExecutionTime () שיטה.

ואז, מה אם נרצה לתזמן משימה זו בכל שנייה במצב עיכוב קבוע? נצטרך להשתמש בגרסה העמוסה של לוח זמנים() דיברנו קודם:

טיימר חדש (). לוח זמנים (NewsletterTask חדש (), 0, 1000); עבור (int i = 0; i <3; i ++) {Thread.sleep (1000); }

כמובן, אנו עורכים את הבדיקות רק לכמה מקרים:

דוא"ל נשלח בתאריך: 2020-01-01T10: 50: 30.860 דוא"ל נשלח בתאריך: 2020-01-01T10: 50: 31.860 דוא"ל נשלח בתאריך: 2020-01-01T10: 50: 32.861 דוא"ל נשלח בתאריך: 2020-01-01T10: 50 : 33.861

כפי שאנו רואים, יש לפחות שנייה אחת בין כל ביצוע, אך לעיתים הם מתעכבים באלפית שנייה. תופעה זו נובעת מההחלטה שלנו להשתמש בחזרה עם עיכוב קבוע.

3.2. עם תעריף קבוע

עכשיו, מה אם היינו משתמשים בחזרה בקצב קבוע? ואז נצטרך להשתמש ב- schedulatedAtFixedRate () שיטה:

טיימר חדש (). scheduleAtFixedRate (NewsletterTask חדש (), 0, 1000); עבור (int i = 0; i <3; i ++) {Thread.sleep (1000); }

הפעם, ההוצאות להורג לא מתעכבות על ידי הקודמות:

דוא"ל נשלח בתאריך: 2020-01-01T10: 55: 03.805 דוא"ל נשלח בתאריך: 2020-01-01T10: 55: 04.805 דוא"ל נשלח בתאריך: 2020-01-01T10: 55: 05.805 דוא"ל נשלח בתאריך: 2020-01-01T10: 55 : 06.805

3.3. קבעו משימה יומית

הבא, בואו להפעיל משימה פעם ביום:

@Test public void givenUsingTimer_whenSchedulingDailyTask_thenCorrect () {TimerTask repeatTask = TimerTask new () {public public run () {System.out.println ("משימה שבוצעה" + תאריך חדש ()); }}; טיימר טיימר = טיימר חדש ("טיימר"); עיכוב ארוך = 1000L; תקופה ארוכה = 1000L * 60L * 60L * 24L; timer.scheduleAtFixedRate (משימה חוזרת, עיכוב, נקודה); }

4. בטל שָׁעוֹן עֶצֶר ו TimerTask

ניתן לבטל ביצוע של משימה בכמה דרכים:

4.1. בטל את TimerTask בְּתוֹך לָרוּץ

על ידי קריאה ל TimerTask.cancel () שיטה בתוך לָרוּץ() יישום השיטה TimerTask עצמה:

@Test ציבורי בטל givenUsingTimer_whenCancelingTimerTask_thenCorrect () זורק InterruptedException {TimerTask task = TimerTask new () {public void run () {System.out.println ("משימה שבוצעה" + תאריך חדש ()); לְבַטֵל(); }}; טיימר טיימר = טיימר חדש ("טיימר"); timer.scheduleAtFixedRate (משימה, 1000 ליטר, 1000 ליטר); Thread.sleep (1000L * 2); }

4.2. בטל את שָׁעוֹן עֶצֶר

על ידי קריאה ל Timer.cancel () שיטה על א שָׁעוֹן עֶצֶר לְהִתְנַגֵד:

@Test public void givenUsingTimer_whenCancelingTimer_thenCorrect () זורק InterruptedException {TimerTask task = TimerTask new () {public void run () {System.out.println ("משימה שבוצעה" + תאריך חדש ()); }}; טיימר טיימר = טיימר חדש ("טיימר"); timer.scheduleAtFixedRate (משימה, 1000 ליטר, 1000 ליטר); Thread.sleep (1000L * 2); timer.cancel (); }

4.3. עצור את החוט של TimerTask בְּתוֹך לָרוּץ

אתה יכול גם לעצור את החוט בתוך לָרוּץ שיטת המשימה, ובכך לבטל את המשימה כולה:

@ מבט הריק הציבורי givenUsingTimer_whenStoppingThread_thenTimerTaskIsCancelled () זורק InterruptedException {TimerTask task = TimerTask new () {run public public run () {System.out.println ("משימה שבוצעה" + תאריך חדש ()); // TODO: עצור את השרשור כאן}}; טיימר טיימר = טיימר חדש ("טיימר"); timer.scheduleAtFixedRate (משימה, 1000 ליטר, 1000 ליטר); Thread.sleep (1000L * 2); }

שימו לב להוראות TODO ב לָרוּץ יישום - על מנת להריץ את הדוגמה הפשוטה הזו, נצטרך לעצור את השרשור.

ביישום חוטים מותאם אישית בעולם האמיתי, יש לתמוך בהפסקת השרשור, אך במקרה זה אנו יכולים להתעלם מהשימוש ולהשתמש בפשטות. תפסיק ממשק API בכיתה Thread עצמה.

5. שָׁעוֹן עֶצֶר לעומת שירות ExecutorService

אתה יכול גם לעשות שימוש טוב ב- ExecutorService כדי לתזמן משימות טיימר, במקום להשתמש בטיימר.

הנה דוגמה מהירה להפעלת משימה חוזרת במרווח מוגדר:

@ מבחן הריק הציבורי givenUsingExecutorService_whenSchedulingRepeatedTask_thenCorrect () זורק InterruptedException {TimerTask repeatTask = TimerTask new () {run public public run () {System.out.println ("משימה שבוצעה" + תאריך חדש ()); }}; ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor (); עיכוב ארוך = 1000L; תקופה ארוכה = 1000L; executor.scheduleAtFixedRate (repeatTask, delay, period, TimeUnit.MILLISECONDS); Thread.sleep (עיכוב + תקופה * 3); executor.shutdown (); }

אז מה ההבדלים העיקריים בין שָׁעוֹן עֶצֶר וה שירות ExecutorService פִּתָרוֹן:

  • שָׁעוֹן עֶצֶר יכול להיות רגיש לשינויים בשעון המערכת; ScheduledThreadPoolExecutor לא
  • שָׁעוֹן עֶצֶר יש לה רק חוט ביצוע אחד; ScheduledThreadPoolExecutor ניתן להגדיר עם מספר שרשורים
  • חריגות זמן ריצה שנזרקו בתוך TimerTask להרוג את השרשור, כך שהמשימות המתוזמנות לא יתקיימו יותר עם ScheduledThreadExecutor - המשימה הנוכחית תבוטל, אך השאר ימשיכו לפעול

6. מסקנה

מדריך זה המחיש את הדרכים הרבות בהן תוכלו להשתמש בפשוט אך הגמיש שָׁעוֹן עֶצֶר ו TimerTask תשתית מובנית ב- Java, לתזמון משימות במהירות. ישנם כמובן פתרונות הרבה יותר מורכבים ושלמים בעולם ג'אווה אם אתה זקוק להם - כמו ספריית קוורץ - אך זהו מקום טוב מאוד להתחיל בו.

ניתן למצוא את היישום של דוגמאות אלה בפרויקט GitHub - זהו פרויקט מבוסס Eclipse, כך שיהיה קל לייבא ולהפעיל אותו כפי שהוא.